Part 7: Internal Service Discovery¶
Add an auth service that the backend calls server-to-server, and understand how AppMecca services address each other.
Use ${service:name} — the :internal form is currently unreliable
An internal URL carries the host port the target service happened to be using when your container started. That port changes every time the target service is redeployed (a new container is brought up on a temporary port before the old one is removed), so the URL stops working the moment the service you are calling is next deployed. Redeploying the calling service fixes it — until the next time.
Recommendation for this tutorial and for production: use the plain
${service:name} form. It resolves to the service's public HTTPS URL,
which is stable across redeploys. The extra hop through AppMecca's HTTPS
router is a good trade for a URL that does not silently break.
If you want the internal path anyway, declare both and fall back in
code: an app that holds only the :internal form will get
connection-refused after the callee's next deploy.
Tracked as a platform bug; a proper fix needs a stable internal address that survives a redeploy.
What you'll learn¶
- Creating a supporting Spring Boot microservice
- The difference between the public and internal service URL templates
- Why
${service:name}is the form to reach for - What
visibilityactually does — and why it is an app-wide setting
What you'll need¶
- A running deployment from Part 6 (or any earlier part with a backend)
Public vs internal URLs¶
AppMecca provides two URL patterns for inter-service communication:
| Template | Resolves to | Notes |
|---|---|---|
${service:backend} |
https://todo-app-backend-yourgroup.dev.example.com |
Routed by the AppMecca HTTPS router. Stable across redeploys — prefer this. |
${service:backend:internal} |
http://backend.todo-app.dev.appmecca.internal:8214 |
Resolves only on AppMecca's private network. The port is the allocated host port, not the container port, and it changes on every redeploy. |
Internal names are served by AppMecca's private DNS and skip the HTTPS router entirely, so there is no TLS handshake — but see the warning above: the port they carry is not stable.
When to use each
${service:name}— the default answer. Works from the browser, works server-to-server, survives redeploys of the service you are calling.${service:name:internal}— only for latency-critical, server-to-server calls where you have accepted the staleness caveat and coded a fallback.
Create the auth service¶
Add a new Spring Boot module at auth-service/ with a standard AuthApplication main class and a controller:
// auth-service/src/main/java/com/example/auth/AuthController.java
package com.example.auth;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
public class AuthController {
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "healthy");
}
@GetMapping("/auth/validate")
public Map<String, Object> validate(@RequestHeader("Authorization") String token) {
// Simplified validation for tutorial purposes
if (token == null || !token.startsWith("Bearer ")) {
return Map.of("valid", false, "error", "Missing or invalid token");
}
return Map.of(
"valid", true,
"user_id", "user-123",
"email", "user@example.com"
);
}
@GetMapping("/auth/permissions")
public Map<String, Object> permissions(@RequestParam String userId) {
return Map.of(
"user_id", userId,
"roles", new String[]{"user", "editor"},
"can_delete", true
);
}
}
Write the auth service Dockerfile¶
# auth-service/Dockerfile
FROM gradle:8-jdk21 AS build
WORKDIR /app
COPY build.gradle settings.gradle ./
COPY src ./src
RUN gradle bootJar --no-daemon
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
Call the auth service from the backend¶
Create a client in the backend that calls the auth service using the internal URL:
// backend/src/main/java/com/example/todo/auth/AuthClient.java
package com.example.todo.auth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@Component
public class AuthClient {
private final RestTemplate restTemplate = new RestTemplate();
@Value("${AUTH_SERVICE_URL:http://localhost:8081}")
private String authServiceUrl;
@SuppressWarnings("unchecked")
public Map<String, Object> validateToken(String token) {
return restTemplate.getForObject(
authServiceUrl + "/auth/validate",
Map.class
);
}
@SuppressWarnings("unchecked")
public Map<String, Object> getPermissions(String userId) {
return restTemplate.getForObject(
authServiceUrl + "/auth/permissions?userId=" + userId,
Map.class
);
}
}
AUTH_SERVICE_URL
The backend reads AUTH_SERVICE_URL from its environment. AppMecca resolves the ${service:auth:internal} template to the private DNS name at deploy time.
Add an auth check endpoint to the backend¶
// backend/src/main/java/com/example/todo/TodoController.java (add method)
@GetMapping("/api/me")
public Map<String, Object> me(@RequestHeader("Authorization") String token) {
Map<String, Object> validation = authClient.validateToken(token);
if (!(boolean) validation.get("valid")) {
throw new RuntimeException("Invalid token");
}
String userId = (String) validation.get("user_id");
Map<String, Object> permissions = authClient.getPermissions(userId);
return Map.of(
"user", validation,
"permissions", permissions
);
}
Update appmecca.yaml¶
Add the auth service container and point the backend at it:
database:
enabled: true
redis:
enabled: true
containers:
- name: backend
dockerfile: backend/Dockerfile
context: backend/
- name: frontend
dockerfile: frontend/Dockerfile
context: frontend/
- name: auth
dockerfile: auth-service/Dockerfile
context: auth-service/
deploy:
- name: backend
container: backend
port: 8080
path: /api
health_check:
path: /health
environment:
AUTH_SERVICE_URL: "${service:auth:internal}"
- name: frontend
container: frontend
type: spa
command: 'nginx -g "daemon off;"' # serves the built bundle from a container
spa_root: /usr/share/nginx/html
port: 3000
health_check:
path: /
environment:
PUBLIC_API_URL: "${service:backend}"
- name: auth
container: auth
port: 8081
health_check:
path: /health
- name: worker
container: backend
command: "java -jar app.jar --worker.enabled=true"
resources:
memory: 512m
How internal resolution works¶
AppMecca registers every service in its own private DNS zone ({app}.{env}.appmecca.internal). The ${service:auth:internal} template resolves to a name like http://auth.todo-app.dev.appmecca.internal:8214 that only resolves on AppMecca's private network, letting the backend reach the auth service without a trip through the HTTPS router.
The trailing number is the allocated host port, not the 8081 you declared — the private DNS record points at the host the container is running on, and each container is given a host port from a managed range. That port is reassigned on every redeploy, which is exactly why the warning at the top of this page recommends ${service:auth} instead.
visibility is app-wide, not per-service
You may be tempted to add visibility: private to the auth service to keep it off the public internet. Read this first.
visibility is declared per service in the YAML, but AppMecca resolves it to a single app-level value using most-restrictive-wins: if any service in the app declares private, the entire app — frontend included — becomes private, and your users hit a login redirect on the todo list.
What private actually does is route the app through AppMecca's auth gateway: anonymous visitors are redirected to a login page, and signed-in visitors are authorised against the owning group's membership. It does not remove the service's hostname.
Flip it after registration instead, when you mean the whole app:
mecca app protect todo-app # whole app behind the auth gateway
mecca app publish todo-app # back to public
A service with no port at all has no hostname to begin with — that is the way to keep something genuinely off the public router.
Build and deploy¶
Verify communication¶
Check services are running and test the endpoint that calls the auth service:
mecca app show todo-app
curl -H "Authorization: Bearer test-token" \
https://todo-app-backend-yourgroup.dev.example.com/api/me
mecca logs todo-app --type container --service backend --since 5m
mecca logs todo-app --type container --service auth --since 5m
Command reference¶
| Command | Purpose |
|---|---|
mecca app show <app> |
Verify all services running, see URLs |
mecca logs <app> --type container --service <svc> |
View one service's output |
mecca queue list -a <app> |
List in-flight actions |
mecca queue status <action-id> |
Check one action |
mecca app protect <app> / publish <app> |
Toggle the whole app's visibility |
What's next¶
In Part 8: Production Patterns, you will configure memory limits, health check tuning, rollback, database backup strategies, and other patterns for running your app in production.