Skip to content

Part 7: Internal Service Discovery

Add an authentication 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 latency of the round trip 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

  • The difference between public and internal service URLs
  • Why ${service:name} is the form to reach for
  • Restricting a service to signed-in members with visibility: private
  • What visibility actually does — and why it is an app-wide setting

What you'll need


Public vs internal URLs

AppMecca provides two URL templates for inter-service communication:

Template Resolves to Notes
${service:backend} https://todo-app-backend-yourgroup.dev.yourdomain.com Goes through the AppMecca HTTPS router. Stable across redeploys — prefer this.
${service:backend:internal} http://backend.todo-app.dev.appmecca.internal:7912 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. They resolve only inside the platform's own network 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.

Step 1: Create the auth service

Add an auth-service/ directory to the repository:

todo-app/
├── backend/
├── frontend/
├── auth-service/
│   ├── app.py
│   ├── requirements.txt
│   └── Dockerfile
└── appmecca.yaml

Create auth-service/requirements.txt:

flask==3.1
pyjwt==2.9
gunicorn==23.0

Create auth-service/app.py:

import os
import jwt
import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET = os.environ.get("JWT_SECRET", "dev-secret-change-me")


@app.route("/health")
def health():
    return jsonify({"status": "healthy"})


@app.route("/auth/validate", methods=["POST"])
def validate_token():
    """Validate a JWT token and return the decoded payload."""
    token = request.json.get("token", "")
    try:
        payload = jwt.decode(token, SECRET, algorithms=["HS256"])
        return jsonify({"valid": True, "user": payload})
    except jwt.InvalidTokenError as e:
        return jsonify({"valid": False, "error": str(e)}), 401


@app.route("/auth/token", methods=["POST"])
def create_token():
    """Create a JWT token for a user (simplified for tutorial)."""
    username = request.json.get("username", "anonymous")
    payload = {
        "sub": username,
        "iat": datetime.datetime.utcnow(),
        "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24),
    }
    token = jwt.encode(payload, SECRET, algorithm="HS256")
    return jsonify({"token": token})

Create auth-service/Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:5000"]

Step 2: Call the auth service from the backend

Add pyjwt and requests to backend/requirements.txt:

requests==2.32
pyjwt==2.9

Create backend/core/auth_client.py:

import os, requests, logging

logger = logging.getLogger(__name__)
AUTH_SERVICE_URL = os.environ.get("AUTH_SERVICE_URL", "http://localhost:5000")

def validate_token(token):
    """Validate a token against the internal auth service."""
    try:
        resp = requests.post(f"{AUTH_SERVICE_URL}/auth/validate", json={"token": token}, timeout=5)
        return resp.json()
    except requests.RequestException as e:
        logger.error(f"Auth service unavailable: {e}")
        return {"valid": False, "error": "Auth service unavailable"}

Traffic path

With ${service:auth-service}, AUTH_SERVICE_URL is a normal HTTPS URL routed by AppMecca. With the :internal form, the name resolves on AppMecca's private network to the host the container is running on, and the request never leaves the platform.

Step 3: Update appmecca.yaml

Add the auth service container and its 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: 8000
    path: /api
    health_check:
      path: /health
    environment:
      CELERY_BROKER_URL: "${redis_url}"
      AUTH_SERVICE_URL: "${service:auth-service: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}"
      PUBLIC_APP_TITLE: "Todo App"

  - name: auth-service
    container: auth-service
    port: 5000
    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: worker
    container: backend
    command: "celery -A config worker --loglevel=info"
    environment:
      CELERY_BROKER_URL: "${redis_url}"

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. Your users would 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, and it does not make the service unreachable.

Flip it after registration instead, when you mean the whole app:

mecca app protect todo-app -e dev     # whole app behind the auth gateway
mecca app publish todo-app -e dev     # back to public

Step 4: Build and deploy

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

mecca app show now lists four services:

backend:      https://todo-app-backend-yourgroup.dev.yourdomain.com
frontend:     https://todo-app-frontend-yourgroup.dev.yourdomain.com
auth-service: https://todo-app-auth-service-yourgroup.dev.yourdomain.com
worker:       (no port — background process, no URL)

Step 5: Verify connectivity

Inspect the auth service's logs to confirm the backend is reaching it:

mecca logs todo-app --type container --service auth-service -e dev
mecca logs todo-app --level error -e dev

URL resolution summary

Template Resolved URL Reachable from Stable?
${service:backend} https://todo-app-backend-yourgroup.dev.yourdomain.com Anywhere Yes
${service:backend:internal} http://backend.todo-app.dev.appmecca.internal:{host-port} AppMecca's private network only No — port changes on redeploy

Internal URLs at build time

Internal URL templates (${service:name:internal}) are skipped at Docker build time because the allocated host port is not yet known. They are only resolved at deploy time when AppMecca injects environment variables — which is also why the value goes stale when the target service is next redeployed onto a different host port.


What's next

Your application is feature-complete. In Part 8: Production Patterns, you will add resource limits, auto-rollback, health check tuning, and production deployment practices.