Skip to content

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 visibility actually 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
        );
    }
}
# auth-service/src/main/resources/application.properties
server.port=8081

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

mecca build trigger -g git@github.com:yourorg/todo-app.git
mecca app deploy todo-app

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.