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
visibilityactually does — and why it is an app-wide setting
What you'll need¶
- A multi-service app from Part 5: Background Workers or later
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:
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:
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:
Step 4: Build and deploy¶
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.