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

  • Adding a supporting microservice
  • The difference between the public and internal 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

  • The full-stack app from Part 6 deployed and working

Public vs internal URLs

AppMecca provides two URL template types:

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:auth:internal} http://auth.todo-app.dev.appmecca.internal:8103 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.

Private DNS

AppMecca maintains a private DNS zone (*.appmecca.internal) for every environment. Internal service records are registered automatically during deployment, pointing at the host each container is running on — which is why the port that goes with the name is the host port, not the container port you declared.

Create the auth service

Create auth-service/ at the repository root alongside backend/ and frontend/:

todo-app/
  auth-service/
    index.js
    package.json
    Dockerfile
  backend/
  frontend/
  appmecca.yaml

Initialise the auth service:

mkdir auth-service && cd auth-service
npm init -y
npm install express jsonwebtoken

Create auth-service/index.js:

const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());

const PORT = process.env.PORT || 3100;
const JWT_SECRET = process.env.JWT_SECRET || "dev-secret";

app.get("/health", (_req, res) => res.json({ status: "healthy" }));

app.post("/auth/validate", (req, res) => {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) return res.status(401).json({ valid: false, error: "No token" });

  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    res.json({ valid: true, user: decoded });
  } catch {
    res.status(401).json({ valid: false, error: "Invalid token" });
  }
});

app.post("/auth/token", (req, res) => {
  const { username } = req.body;
  if (!username) return res.status(400).json({ error: "Username required" });

  const token = jwt.sign({ username, iat: Date.now() }, JWT_SECRET, {
    expiresIn: "24h",
  });
  res.json({ token });
});

app.listen(PORT, () => console.log(`Auth service listening on ${PORT}`));

Create auth-service/Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3100
CMD ["node", "index.js"]

Call the auth service from the backend

Update backend/index.js to validate tokens via the internal auth service:

const AUTH_URL = process.env.AUTH_SERVICE_URL || "http://localhost:3100";

async function validateToken(req, res, next) {
  const token = req.headers.authorization;
  if (!token) return next(); // Allow unauthenticated for this tutorial

  try {
    const resp = await fetch(`${AUTH_URL}/auth/validate`, {
      headers: { authorization: token },
      method: "POST",
    });
    const data = await resp.json();
    if (data.valid) req.user = data.user;
  } catch (err) {
    console.error("Auth service error:", err.message);
  }
  next();
}

app.use(validateToken);

Update appmecca.yaml

Add the auth service container and deploy entry:

database:
  enabled: true

redis:
  enabled: true

containers:
  - name: backend
    dockerfile: backend/Dockerfile
    context: backend/

  - name: frontend
    dockerfile: frontend/Dockerfile
    context: frontend/

  - name: auth-service
    dockerfile: auth-service/Dockerfile
    context: auth-service/

deploy:
  - name: backend
    container: backend
    port: 3000
    path: /api
    health_check:
      path: /health
    environment:
      AUTH_SERVICE_URL: "${service:auth:internal}"

  - name: auth
    container: auth-service
    port: 3100
    health_check:
      path: /health
    # JWT_SECRET is a secret — set it with `mecca var set <app> JWT_SECRET=…`,
    # never in this file, which is committed to your repository.

  - 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: worker
    container: backend
    command: "node worker.js"

Key additions:

  • ${service:auth} resolves to the auth service's public HTTPS URL at build and deploy time — stable across redeploys
  • Secrets are never written into appmecca.yaml. That file is committed to your repository; set secrets with mecca var set 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.

Set the JWT secret

mecca var set todo-app JWT_SECRET="a-long-random-secret-value"

Sensitive values

Variables set with mecca var set are held in the platform secret store and injected into your containers at deploy time — they are never written to your repository. Do not commit secrets.

With no -b, the value is written to every branch of the app. Narrow it with -b <branch> if that is not what you want. Changes take effect on the next deploy.

Build and deploy

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

Verify communication

Exercise the flow through the backend:

BACKEND=https://todo-app-backend-yourgroup.dev.example.com

# Get a token
curl -X POST "$BACKEND/auth/token" \
  -H "Content-Type: application/json" \
  -d '{"username": "alice"}'

# Use the token (the backend validates it via the auth service)
curl "$BACKEND/api/todos" \
  -H "Authorization: Bearer <token>"

Check the auth service's logs to confirm it received the validation request. The service is selected with --service:

mecca logs todo-app --type container --service auth --since 5m

What's next

In Part 8: Production Patterns, you will learn about memory limits, health check tuning, database backup strategies, and rollback.