Skip to content

Domain Routing

AppMecca automatically assigns HTTPS URLs to your deployed services. This page explains how URLs are generated, how HTTPS works, and what happens with multi-service applications.

How URLs Are Assigned

When you deploy an application, AppMecca builds a hostname from four things:

  1. App slug -- derived from your Git repository name
  2. Service name -- the name of the service in your deploy section
  3. Group -- the group that owns the app
  4. Environment domain -- e.g. dev.appmecca.net

The result is:

https://{app}-{service}-{group}.{env}.{domain}

For example, an app called hello-flask owned by the group acme, with one service named web, deployed to dev:

https://hello-flask-web-acme.dev.appmecca.net

The service name is always in the URL — even for a single-service app

There is no bare {app}.{env}.{domain} address. An app with exactly one service still gets {app}-{service}-{group}.{env}.{domain}. If you expected hello-flask.dev.appmecca.net to work, it will not — always read the real URL from mecca app show <app>.

Multi-service apps

Each service with a port gets its own hostname, differing only in the service segment. For example, an app named shortlink declaring services web, fastapi, svelte, and react:

https://shortlink-web-acme.dev.appmecca.net
https://shortlink-fastapi-acme.dev.appmecca.net
https://shortlink-svelte-acme.dev.appmecca.net
https://shortlink-react-acme.dev.appmecca.net

Services without a port don't get URLs

Background workers and other services defined without a port in appmecca.yaml are internal-only. They don't receive any web traffic and don't get a URL.

Never hardcode a URL — read it or template it

Use mecca app show <app> to see every service's real URL, and ${service:name} in appmecca.yaml when one service needs another's address. See Inter-Service Communication.

HTTPS (TLS/SSL)

All traffic to your application is served over HTTPS. AppMecca handles this automatically:

  • AppMecca issues a wildcard TLS certificate covering *.{env}.{domain} when the environment is provisioned
  • The AppMecca HTTPS router terminates TLS and forwards plain HTTP to your containers
  • Your containers do not need to handle HTTPS themselves -- they just serve plain HTTP
User (HTTPS) --> AppMecca HTTPS router (terminates TLS) --> Container (HTTP on port 8000)

No certificate management needed

You never need to create, renew, or manage TLS certificates. The wildcard certificate covers every app in the environment automatically, and AppMecca renews it for you.

Traffic Flow

Here is the full path a request takes from a user's browser to your application:

Browser
  |
  | HTTPS (port 443)
  v
Platform DNS (*.dev.appmecca.net -> the HTTPS router)
  |
  v
AppMecca HTTPS router
  |
  | Matches the hostname -> routes to your service
  v
AppMecca host (a dynamically allocated host port)
  |
  | Docker port mapping
  v
Your Container (the container port you declared, e.g. 8000)

The router inspects the Host header of each request and forwards it to the matching service. Each service is health-checked independently.

The host port is allocated by the platform and changes

AppMecca picks a host port for each container from a reserved range and re-allocates it on every redeploy. You never configure it, and you should never assume a fixed value.

Public and Private Apps

Every app is private by default — a newly registered app is reachable only to members of your group, and everyone else gets an authentication challenge instead of your site. Run mecca app publish <app> to make it public, or click Make public next to the Visibility badge on the app's overview in the dashboard. Being private puts the AppMecca auth gateway in front of the app:

  • an anonymous visitor gets a 302 to the environment's login page, with ?next= so they land back where they started
  • a signed-in user who is not a member of the app's group gets a 403
  • members of the group pass straight through
mecca app protect myapp     # private — auth-gated
mecca app publish myapp     # public — opt in when you're ready
mecca app show myapp        # confirm: the "Visibility" row

Both commands are idempotent and you never need to redeploy. How quickly the change reaches your visitors depends on how your app is served — see How long the flip takes, which is worth reading before your first flip. It ranges from about fifteen seconds to a few minutes.

You can also declare it in appmecca.yaml, per service:

containers:
  - name: web
    dockerfile: Dockerfile

deploy:
  - name: web
    container: web
    port: 8000
    visibility: private

The platform stores a single visibility flag per app and the most restrictive service wins — if any service declares visibility: private, the whole app is private. The appmecca.yaml value is applied when the app is registered; mecca app protect / mecca app publish override it afterwards.

Keep your production branch public

Private mode is meant for a non-production branch that needs gating while you build — a dev or qa URL you want to share with a reviewer but not with the open internet. Custom domains and edge caching only work on a public app (see below), so the branch serving your real traffic should stay public.

What the gateway passes to your app

A request that goes through the gateway reaches your app almost exactly as it was sent. That covers every request to a private app, and every request to a function service. A few request forms are refused or changed on the way:

  • CONNECT is refused with 400 and an empty body. It never reaches your app.
  • OPTIONS * is refused with 404 and the plain-text body Not Found. An OPTIONS request for a real path, such as /api/items, is handled like any other request.
  • A POST, PUT or PATCH with no body reaches your app with content-length: 0 added. A request that already carries a length is not changed.
  • Your cookies are passed through, including one whose value holds non-ASCII characters. The gateway removes only the platform's own sign-in cookies, so your app never sees a visitor's AppMecca session.

There are only two app shapes — and caching decides the rest

Everything about visibility follows from how your app is served, and there are only two ways:

Shape What it is Served from
Container Anything that runs as a process — a backend, an API, a worker, or a server-side-rendered frontend. An ordinary service (and type: ssr). Your container, behind the HTTPS router
Static Files built once and uploaded — type: static, and type: spa builds. No process runs. AppMecca object storage, behind the edge cache

The distinction that then matters is whether the edge cache sits in front of your hostname, because the edge cache is a second front door and the auth gateway only guards the HTTPS router.

  • A static app always has one. There is no other way to serve it, so edge caching is inherent — you do not switch it on and you cannot switch it off. The default lifetime for a cached file is 24 hours.
  • A container app has one only if you asked for it, with a cache: block on the service. Without that block your hostname points straight at the HTTPS router and nothing is cached anywhere.

That gives three cases, and they behave differently:

Container, no cache: block   Browser -> HTTPS router -> gateway -> your container
Container, cache: block      Browser -> edge cache -> HTTPS router -> gateway -> container
Static (always)              Browser -> edge cache -> object storage

Container without a cache: block — the simple case

Going private re-points the routing rule for your hostname at the gateway. Nothing else about how the app is served changes, no DNS record moves, and nothing is cached anywhere that could outlive the change. This is the fastest and cleanest flip: about fifteen seconds, and there is no window in which the old behaviour lingers.

Container with a cache: block — caching is suspended while private

A cached response is served from the edge without contacting your app or the gateway — which is exactly why caching and gating cannot both apply to the same path. So while the app is private, the platform suspends your caching for you: every path is switched to pass-through, so every request reaches the gateway with cookies and headers intact, and the cache is cleared. Going public restores exactly the caching you declared.

You do not have to configure anything for this, and you do not have to choose between the two. Two consequences are worth knowing:

No edge caching while private, and a short window on the flip

A private app is not edge-cached, deliberately — caching an authenticated response at a shared edge is the thing the auth gate exists to prevent. It is served from the environment's region instead, so a private app is measurably slower than the same app public. Private static sites behave the same way, for the same reason.

Clearing the cache is not instantaneous. Changing the caching rules does not by itself evict what is already stored, so the flip also clears the cache — and that takes a couple of minutes. Until it finishes, a path that was cached while the app was public can still be served from the edge. If you need something unreachable immediately, undeploy it rather than making it private.

The simplest configuration avoids all of this

If no path has a cache lifetime, none of the above applies — there is nothing to suspend and nothing to clear:

cache:
  enabled: true
  default_ttl: 0     # the default — nothing is cached
  # and no `behaviors:` entries, because a behaviour's `ttl`
  # defaults to 86400 (24 hours) when you omit it

That is also what a bare cache: {enabled: true} gives you. It passes every request through to your app, which is what most apps want.

Static — the edge cache is taken out of the path

A static app is always served through the edge cache. Going private takes the edge cache out of the request path entirely — the DNS record for your hostname moves from the edge cache to the HTTPS router, and a request is then served like this:

Browser
  |
  | HTTPS
  v
Platform DNS (myapp-web-mygroup.dev.appmecca.net -> HTTPS router, not the edge)
  |
  v
AppMecca HTTPS router
  |
  v
Auth gateway  --> 302 to login if you have no session
  |               403 if you are not in the app's group
  | (authenticated)
  v
Your built files, read from private AppMecca object storage

Moving the DNS record is not enough on its own, because the edge cache would still answer for anyone who reaches it directly — so while the app is private the platform also switches the edge cache off for that hostname. That is what makes the gateway the only way in, which is the guarantee a container app gets for free.

It is switched off, never deleted, and your built files stay where they are. mecca app publish myapp switches it back on, waits for it to be ready, and only then moves DNS back to it — in that order, so your visitors are never sent to an edge cache that is not serving yet. You never rebuild or redeploy to get it back.

Because the edge cache is bypassed, everything attached to it is bypassed with it. That produces two consequences worth understanding before you flip a static app.

1. No edge caching while private

Every request travels browser → HTTPS router → gateway → object storage, and is served from the environment's own region rather than from a location near your visitor. Responses are also marked Cache-Control: no-cache, so browsers revalidate instead of reusing what they already have. A private static site is measurably slower than the same site served publicly.

This is deliberate, not a limitation waiting to be lifted: caching an authenticated response at a shared edge is exactly the thing the auth gate exists to prevent. If you need edge performance, the app needs to be public.

Directory-style URLs stop resolving while private

Serving through the edge cache includes a small function that rewrites /page/ to /page/index.html. That function lives on the edge, so private mode bypasses it too. A site whose generator emits directory-style URLs (MkDocs with its default use_directory_urls, Hugo, Jekyll) will serve / fine but 404 on sub-pages while private. Sites with spa_fallback enabled — React, Vue, Svelte builds — are unaffected, because the fallback re-serves the entry document for any unmatched path.

2. No custom domains on a private app

For a static app, a custom domain's certificate and alias attach to the edge cache — the thing private mode bypasses. So the platform keeps the two mutually exclusive, in both directions:

  • mecca domain add on a private app is refused with a 409, telling you to run mecca app publish <app> first. This applies to any private app, container or static.
  • mecca app protect is refused with a 409 while the app has any active custom domain.

If you genuinely want to drop the domains and go private, say so explicitly:

mecca app protect myapp --force-remove-domains

That starts teardown of each attached custom domain and then performs the flip. It is destructive — the domains are gone, and re-adding them after you go public again means re-doing the DNS records and waiting for a fresh certificate. There is no equivalent flag on mecca app publish; going public never needs to destroy anything.

If the URL still looks public

A static app can only be flipped once it has actually been deployed — the platform needs a live edge cache to park and a set of built files to serve. If you run mecca app protect on a static app that has never had a successful deploy, there is nothing to re-point and the URL keeps serving as before. Deploy first, then flip.

To re-assert the current visibility without changing it:

mecca app reconcile myapp
mecca app show myapp        # check the "Visibility" row

How long the flip takes

A container app with no cache: block changes in about fifteen seconds — up to a minute if the gateway's routing table has only just refreshed — and the rest of this section does not apply to it: no DNS record moves, so there is nothing for a visitor's machine to remember.

A static app is different, and the difference is not a fault. Its flip moves a DNS record, and DNS answers are cached — by resolvers, and by browsers, which keep their own cache independent of the record's 60-second lifetime. So for a few minutes after the flip, two visitors can legitimately see two different things: anyone resolving the name fresh gets the new behaviour immediately, while anyone whose machine already looked it up keeps the old behaviour until their cache expires.

That includes you. You are the most likely person to have visited the site just before flipping it, so you are the most likely to see the stale view — which is why it reads like a broken deploy when it is not.

The two directions produce opposite symptoms:

You flipped Someone with a stale DNS answer sees Why
public → private the site still loading normally their machine still points at the edge cache, which serves for a short while longer
private → public a login prompt, on an app that is now public their machine still points at the HTTPS router, where the gateway is still challenging

Neither is fixable from our side — the visitor's own resolver is holding an address we no longer want it to use. Both clear themselves.

What to do: wait two or three minutes and reload. If you want to see the change immediately in your own browser, clear its DNS cache — in Chrome, open chrome://net-internals/#dns and choose Clear host cache. A hard reload alone will not help, because the stale entry is a DNS answer, not a cached page.

Going private is not instantaneous, on purpose

When you make a static app private the platform moves DNS first and only switches the edge cache off once visitors have had time to notice — roughly fifteen minutes. Switching it off sooner would give anyone with a cached address a hard error instead of the site, and an error is a worse outcome than briefly still serving content that was public a moment ago. Access through the gateway is gated from the moment the flip lands; the delay only affects visitors who have not yet re-resolved the name.

Do not use a visibility flip as an incident response

If content must become unreachable right now — the wrong data shipped, a leak — do not rely on going private, for any app shape. Undeploy it (mecca app undeploy myapp), which stops serving at the source rather than changing where visitors are pointed.

The gateway itself is always public

mecca app protect gateway is refused. The gateway is what authenticates everyone else; gating it behind itself would produce a login loop.

Custom Domains

Custom domains are additive — your service is accessible on BOTH the system-generated URL (myapp-web-acme.dev.appmecca.net) AND your custom domain (www.example.com). The system domain never goes away.

Custom domains require a public app

mecca domain add is refused with a 409 on an app whose visibility is private, and mecca app protect is refused while any custom domain is active. See Public and Private Apps for why, and for the --force-remove-domains escape hatch.

Adding a Custom Domain

Custom domains are managed exclusively via CLI or API. This ensures the full certificate lifecycle (issuance, DNS validation, attaching the certificate to the router or edge cache) is handled correctly — these steps cannot be driven from a config file alone.

# Add a custom domain to a service (-s is required)
mecca domain add myapp www.example.com -s web -e dev

# Check certificate validation status
mecca domain verify myapp www.example.com -e dev

# List all custom domains
mecca domain list myapp -e dev

# Remove a custom domain
mecca domain remove myapp www.example.com -e dev

The app name comes first, then the domain; the service is the -s/--service option and is required on add.

The domain / domains fields in appmecca.yaml do nothing

Both fields are accepted by the schema for forward compatibility, but they are silently ignored during deployment — nothing warns you at deploy time that they had no effect. Custom domains must be added via mecca domain add so that certificate issuance and DNS validation can actually happen.

Unknown keys in appmecca.yaml are ignored silently

This is not specific to domains. AppMecca ignores any key it does not recognise anywhere in appmecca.yaml — no error, no warning. A typo like helth_check: or replica: simply does nothing, and the service deploys with the default. Double-check spelling against Configuration: Services when a setting appears to have no effect.

How It Works — the uniform two-record procedure

Every custom domain needs exactly one traffic record + one validation record:

Add these TWO DNS records at your domain provider:

  1. Traffic:
www.example.com  CNAME  myapp-web-mygroup.dev.appmecca.net

  2. Certificate validation (one-time):
_acme-challenge.www.example.com  CNAME  _acme-challenge.www.example.com.acme-delegated.dev.appmecca.net

The validation record delegates certificate validation to the platform once: the TLS certificate is issued automatically — even before you flip traffic — and renews forever with no further DNS changes. mecca domain verify <app> <domain> live-checks both records and tells you exactly which one is still missing.

Both records — with the record type correct for your domain (CNAME, or A/ALIAS at an apex) — are shown on the dashboard's Domains tab, each with Copy name and Copy value buttons and a Check DNS button that live-resolves it.

Copy the record NAME with the button — don't hand-select it

A record name pasted with an invisible extra character (most often a leading space picked up by a mouse selection) is stored by your DNS provider under a different name that renders identically in the console — Route 53, for example, stores it as \040_acme-challenge.… and shows no visible difference. The record then never resolves, while your provider truthfully shows it as present. Use the Copy name button (dashboard), or copy from the flush-left record lines the CLI prints. If it happens anyway, Check DNS / mecca domain verify will detect the common mispastes and name the nearly-identical record it found, with the exact fix: delete it and recreate it with the name copied exactly.

A record that says \"not resolving yet\" but the cert issues anyway

Certificate issuance asks your domain's authoritative nameservers. A public resolver may still be serving a cached "does not exist" answer from before you added the record (DNS negative caching, up to 24 h with some providers). When AppMecca sees that shape it says so explicitly — "correct at your nameservers; a stale DNS cache is clearing" — and proceeds with issuance. Nothing to do but wait for the cache to expire; your visitors on other resolvers are unaffected.

Custom domains live on the production branch

A custom domain belongs to the project, not to one branch — and it is served by your production branch (the one marked with mecca app set-production; main by default). So custom domains are viewed and added only from that branch:

  • The dashboard's Domains tab shows the System hostnames for every branch — each branch has its own automatic hostname, which is how you preview a feature branch.
  • The Custom domains section and the Add domain form appear only on the production branch. On any other branch you'll see a note telling you which branch to switch to.
  • The API refuses a custom-domain write aimed at a non-production branch with a 409.

Promoting a different branch does not move your domain yet

Today a custom domain is served by main. Running mecca app set-production on another branch updates the flag but does not re-point your live custom domain — that is coming (it is a tracked feature). Until then, keep your production branch as the one your domain should serve.

Apex (root) domains

DNS forbids CNAME at the apex (example.com), so the traffic record forks by what your DNS provider supports — the validation record stays a normal CNAME everywhere:

Your DNS provider Traffic record at the apex
Supports ALIAS / ANAME / CNAME-flattening (Cloudflare, Namecheap, Porkbun, DNSimple, Gandi, DNS Made Easy, easyDNS, Squarespace DNS) ALIAS example.com → {platform hostname} — apex serves directly
No ALIAS support (GoDaddy, OVH, IONOS, Hostinger, most registrar-default DNS) A example.com → {the environment's fixed ingress IPs} — printed by mecca domain add

The exact record mecca domain add prints is the authoritative one — it is computed for your provider, your app shape, and your apex policy.

Choose what the apex does

An apex can redirect to www. or serve your site itself. You pick; we recommend redirecting.

mecca domain add myapp example.com -s web                  # 301 → www (recommended)
mecca domain add myapp example.com -s web --apex serve     # answers at example.com
mecca domain set-apex myapp example.com serve              # change your mind later

Redirect gives you one canonical address (better for SEO — no duplicate-content split), one certificate path, and it works with every DNS provider. It is what Vercel, Netlify and Render all recommend. When you choose it, mecca domain add example.com automatically sets up the www.example.com sibling and prints its records too.

Serve answers directly at the root. For a container app it just works. For a static site your DNS provider must support ALIAS/ANAME — the content is served through the edge cache, whose addresses are not stable, so an A record cannot reach it whatever it points at. If your registrar has no ALIAS support (GoDaddy and friends), choose redirect or move DNS to us with mecca domain ns-setup.

Defaults, if you say nothing: a static site redirects (it is the only thing it can do without ALIAS); a container serves at the apex.

Changing the policy is a visible cutover

mecca domain set-apex makes the redirect appear or disappear, and the apex traffic record changes shape (A ↔ ALIAS). The domain returns to "waiting for DNS" until you republish it; the certificate then re-issues automatically. Your www. host is never touched.

Certificate auto-renewal

Certificates renew automatically as long as the _acme-challenge validation CNAME stays in place. You never need to manually renew, and renewals never require new DNS records.

Cloudflare users

If your domain is proxied through Cloudflare (orange cloud), set SSL/TLS mode to Full — "Flexible" causes redirect loops.

Full DNS delegation (NS hosting)

If you'd rather AppMecca manage all of your domain's DNS — so the apex Just Works and you never touch a DNS record again — delegate your domain's nameservers to us once. This is a chargeable add-on best for apex-heavy setups or customers who want a single place to manage everything.

# 1. Scan your existing DNS and stage the import (nothing changes at
#    your registrar yet). Prints the 4 nameservers to set.
mecca domain ns-setup example.com --force

# 2. Watch for activation (we detect when your registrar's NS flips).
mecca domain ns-status example.com

# Later, to hand DNS back to another provider:
mecca domain ns-release example.com

Your email keeps working. Before you change a single nameserver, ns-setup scans your live DNS — every MX, SPF/TXT, DKIM, and A/CNAME/SRV/CAA record — and imports it into the AppMecca zone first, so when you flip the NS at your registrar the switch is seamless. Review the imported record list ns-setup prints before you change anything; anything unusual (a rarely-used subdomain, a non-standard DKIM selector) you can add afterward with the platform once delegated.

Once your registrar's nameservers point at us and we detect the flip (ns-status shows active), certificates issue and renew with zero DNS records on your side — validation happens entirely inside your AppMecca-hosted zone.

Change nameservers only after the import completes

Wait until mecca domain ns-status example.com shows the imported record set and hands you the four nameservers. Flipping NS before the import lands is the one way to drop email during a migration.

ns-status works from any of your groups

A domain is globally unique and belongs to one group, so ns-status authorizes you by membership of that group — you no longer have to switch your active group to check it. Previously it replied "Domain 'example.com' is not NS-hosted." to the domain's own owner if their active group was a different one of their own, which was not merely unhelpful but untrue.

ns-setup and ns-release are deliberately not the same. Both write ownership — one stamps it, the other irreversibly deletes the zone — so they still act on your active group and want an explicit --group when you have more than one.

URL Patterns at a Glance

Every hostname carries the service name; the branch segment appears only on a non-production branch.

Scenario URL Pattern Example
Any service, production branch {app}-{service}-{group}.{env}.{domain} myapp-web-acme.dev.appmecca.net
Multi-service (frontend) {app}-{service}-{group}.{env}.{domain} myapp-frontend-acme.dev.appmecca.net
Multi-service (backend) {app}-{service}-{group}.{env}.{domain} myapp-backend-acme.dev.appmecca.net
Non-production branch {app}-{branch}-{service}-{group}.{env}.{domain} myapp-feature-payments-backend-acme.dev.appmecca.net
Custom domain (additive) System URL + custom domain myapp-web-acme.dev.appmecca.net AND www.example.com

Branch names are sanitised for use in URLs: / becomes -, uppercase becomes lowercase, and special characters are replaced with -. See Branch Deployments for details.

Don't derive it — read it

Group slugs, branch sanitisation, and reserved hostnames all feed into the final name. mecca app show <app> prints the real URL for every service; treat that as authoritative over any pattern in this table.

Environments and Isolation

Each environment has its own set of URLs. The same app deployed to different environments gets different URLs:

dev:     https://myapp-web-acme.dev.appmecca.net
staging: https://myapp-web-acme.staging.appmecca.net
prod:    https://myapp-web-acme.prod.appmecca.net

These are completely isolated -- separate routers, separate hosts, separate databases. Deploying to dev has no impact on staging or prod.

Troubleshooting Routing

App deployed but URL not working

  1. Check that the deployment completed. mecca queue status takes the action ID the deploy printed, not an app name:

    mecca queue status <action-id> -e dev
    
    To see everything in flight for an app instead:
    mecca queue list -a myapp -e dev
    

  2. Confirm the service is healthy and check the URL AppMecca actually assigned:

    mecca app show myapp -e dev
    

  3. Allow 30-60 seconds after deployment for DNS propagation and for health checks to mark the new container healthy.

  4. If the URL you tried has no service segment (myapp.dev.appmecca.net), that is the problem — see How URLs Are Assigned.

Wrong service responding

If you're hitting one service's URL but getting another service's response, check that your services have different health check paths and that your appmecca.yaml service names are correct.

Next Steps