AppMecca AI Agent Guide¶
A single-file, self-contained reference for AI coding agents configuring an application to run on AppMecca.
This guide is written for any AI assistant — it assumes nothing about which model or
tool is reading it. Copy it into your project (e.g. as APPMECCA.md, or append it to
whatever instruction file your assistant reads: CLAUDE.md, AGENTS.md, .cursorrules,
.github/copilot-instructions.md, GEMINI.md, or equivalent) so your agent can
configure appmecca.yaml and the Dockerfile correctly without reading the platform's
source. If you are an AI agent reading this: §21 at the end is your step-by-step
operating procedure — start there, and use §1–§20 as the reference it points into.
Everything below is verified against the platform's live validation code. Where the platform accepts something silently but behaves surprisingly, that is called out as a trap — those are the cases that waste the most time.
0. What AppMecca is, in one page¶
AppMecca takes a Git repository and runs it. You give it:
- A
appmecca.yamlin the repository root — declares what to build and how to run it. - A
Dockerfile(or several) — unless your app is a pure static site, which needs none.
AppMecca then builds your images, provisions whatever backing services you declared (database, cache, storage), deploys your containers across a pool of hosts, puts HTTPS in front of them, and gives you a URL.
You never provision infrastructure, write networking config, or manage credentials. Backing services are declared in one line each and their connection details arrive as environment variables inside your container.
The CLI¶
Everything you do runs through the mecca command: register, build, deploy, logs,
variables, storage, database, domains. Every example in this guide uses it.
Glossary — every term and every feature¶
Structure
| Term | YAML key | Meaning |
|---|---|---|
| App | — | One Git repository + branch, registered with AppMecca. The unit you deploy. |
| Service | deploy[] |
One runnable unit inside an app — a web server, a worker, a static frontend. An app has one or more. |
| Container | containers[] |
One built image. Several services can run from the same image with different commands. |
| Container service | deploy[] with container: |
A service that runs as a long-lived process in a container. |
| Static service | deploy[].type: static |
A service with no container — built once and served from the edge cache. |
| Worker | deploy[] with no port |
An internal service with no URL and no health check (queue consumer, scheduler). |
| Replica | deploy[].replicas |
One additional copy of a service, placed on a different host. 1–3. |
| Branch | — | Each Git branch deploys independently: its own URL, database, cache namespace and storage. |
| Group | — | Your team/tenant. Your app's URL ends with your group's slug. |
| Slug | — | Your app's short name, derived from its display name at registration. Part of its URL. A small set of names is reserved — see below. |
Data and state
| Term | YAML key | Meaning |
|---|---|---|
| Database | database.enabled |
A managed PostgreSQL database, provisioned on first deploy. Credentials arrive as DATABASE_URL. One per app per branch. §5 |
| Application cache (a.k.a. internal cache) | cache.enabled |
A managed Redis key-value store your code talks to directly, for sessions, queues, computed values. Arrives as REDIS_URL. Default off. §6 |
| Drive | storage.drives[] |
A shared POSIX filesystem volume mounted into your container at a path you choose. Ordinary file I/O. Capped at 100 GB. §7.1 |
| Bucket | storage.buckets[] |
Private object storage for your app, reached over the S3 API with an injected credential. Effectively unlimited. §7.2 |
| Backup bundle | — | A point-in-time snapshot of your database and drives (not buckets), taken by mecca app backup. §16 |
Delivery and routing
| Term | YAML key | Meaning |
|---|---|---|
| Edge cache (a.k.a. external cache, web cache, content cache, CDN) | deploy[].cache |
AppMecca's HTTP response cache, sitting in front of a service at the network edge. Caches whole responses by URL path, close to your users. On by default whenever the block is present. §12 |
| Front door | deploy[].primary |
The service whose URL represents the app on the dashboard. Auto-detected unless you set it. §2.6 |
| Visibility | deploy[].visibility |
public (anyone with the URL) or private (only signed-in members of your group). App-wide. §11 |
| Custom domain | — (CLI only) | Your own hostname in front of a service. Managed with mecca domain add — not in YAML. §13 |
| Health check | deploy[].health_check |
The HTTP probe that decides whether your service is alive and whether a deploy succeeded. §2.4 |
cache means two completely different things
This is the single most confusing thing in appmecca.yaml, because both use the key
cache:.
| Application cache (internal) | Edge cache (external / web) | |
|---|---|---|
| Where | Top level: cache: |
Inside a service: deploy[].cache: |
| What it is | Redis key-value store | HTTP response cache at the edge |
| Who talks to it | Your code, via REDIS_URL |
The network, transparently |
| What it stores | Whatever you put in it | Copies of your HTTP responses |
| Default | Off — you must set enabled: true |
On as soon as the block exists |
They are independent. You can use either, both, or neither.
Operations
| Term | Meaning |
|---|---|
| Pre-flight | The repository check that runs at registration: can AppMecca reach the repo, is appmecca.yaml present and valid. An app cannot build or deploy until it passes. §15 |
| Build | Turning your repository into images (and static bundles). |
| Deploy | Starting the new version and switching traffic to it. |
| Action / queue | Every build and deploy is a queued action with an id you can watch with mecca queue status. §15 |
| Environment variable | Configuration delivered into your container — from YAML, from mecca var set, or injected by the platform. §8 |
| Cron job | A scheduled HTTP request AppMecca makes to your own endpoint. Not a command in a container. §10 |
| Built-in email (a.k.a. the email relay) | AppMecca's zero-configuration email service, enabled with email.enabled: true. For notifying yourself — the developer, site owner or team — of things happening in the app: contact-form submissions, threshold alerts, error digests, system events. It is not a bulk/marketing/customer-mailing service. No mail server, no credentials, no third-party account: the app POSTs one JSON request. §9 |
Reserved slugs — you are never refused, you are suffixed¶
An app's slug becomes a DNS label and reaches the path positions the platform's own API answers on, so two families of name are reserved:
- the platform's hostnames —
api,dashboard,docs,admin-docs,dev-docs,api-docs,tutorial,monitor,gateway,screenshot,mail - the literal segments of its API routes —
apex-check,checks,claim,config-schema,group,history,hosts,id,incidents,ns,pool,prioritize,providers,queue,refresh,report,snooze,stable,summary,suppressions,uptime,version,versions
⚠ Do not hardcode that list. It is DERIVED from the live route tree and the platform's hostname map, and a guard test fails the build when the two drift apart — so it changes when the platform changes. Ask for the name you want.
Registration never fails because of this. A reserved name — or one another app in your group already holds — is registered on the next free numbered variant, and the substitution is reported back to you:
Suffixes run -1, -2, -3. Apps registered before the rule existed keep their
slug; nothing is renamed retroactively.
1. Decision tree — pick your app shape first¶
Everything else follows from this. Read it before writing any YAML.
Does your app need a server process at runtime?
│
├─ NO — it's HTML/CSS/JS that a browser can run directly
│ (React, Vue, Svelte, Angular SPA; Hugo, MkDocs, Jekyll, Astro static export)
│ │
│ └─► STATIC APP. type: static + static_site:
│ No Dockerfile. No containers: block. No port. No health check.
│ Served from AppMecca's edge cache. §4.2
│
├─ YES, and it's the only thing
│ │
│ └─► CONTAINER APP. containers: + deploy: with a port
│ One Dockerfile, one service. §4.1
│
└─ YES for the backend, NO for the frontend (an API + a separate SPA)
│
└─► MIXED APP. One container service + one type: static service
The most common full-stack shape. §4.3
Do not put a React build behind an nginx container just to serve files. Use
type: static — it is cheaper, faster, globally cached, and needs no Dockerfile.
2. appmecca.yaml — complete reference¶
The file must be named exactly appmecca.yaml and live in the repository root.
(A different path is possible via mecca register --config-path sub/appmecca.yaml.)
2.0 THE #1 TRAP: unknown keys are silently ignored¶
The config parser drops any key it does not recognise, with no error and no warning.
deploy:
- name: web
container: web
replicaz: 3 # ← typo. Silently ignored. You get replicas: 1.
helth_check: # ← typo. Silently ignored. No health check at all.
path: /health
There is no spell-check and no strict mode. Every key you write must match this reference exactly. When something you configured "does nothing", suspect a typo first.
2.1 Top-level keys¶
| Key | Required | Purpose | Section |
|---|---|---|---|
app_name |
No | Display-name override. Defaults to the repository name. | — |
containers |
Conditional | Images to build. Omit only when every service is type: static. |
§2.2 |
deploy |
Yes | The services that make up your app. At least one. | §2.3 |
database |
No | Managed PostgreSQL database. | §5 |
cache |
No | Managed Redis key-value cache. | §6 |
storage |
No | Drives and buckets. | §7 |
static_site |
Conditional | Build settings for static services. Required if any service is type: static (unless every such service carries its own). |
§4.2 |
static |
No | Collect-static asset upload alongside a container service. Rarely needed. | §2.7 |
email |
No | Built-in email — the app notifies its owner (forms, alerts). | §9 |
cron |
No | Scheduled HTTP requests to your own endpoints. | §10 |
cache and static each name two different things
- Top-level
cache:= managed Redis. Default off. - Per-service
deploy[].cache:= HTTP edge caching. Default on whenever the block is present — writingcache: {}under a service enables edge caching. - Top-level
static:= collect-static upload beside a container. - Top-level/per-service
static_site:= the static deploy target instead of a container.
Confusing either pair is a common and confusing failure. See §6, §2.7 and §4.2.
2.2 containers[] — what to build¶
containers:
- name: web # required — referenced by deploy[].container
dockerfile: Dockerfile # required (see trap below)
context: . # default "."
args: # optional build args
NODE_ENV: production
target: runtime # optional multi-stage build target
| Field | Required | Default | Notes |
|---|---|---|---|
name |
Yes | — | Lowercase, hyphens. Must be unique. Referenced by deploy[].container. |
dockerfile |
Yes | Dockerfile |
The schema defaults it, but pre-flight validation rejects the key being absent. Always write it. |
context |
No | . |
Build context, relative to repo root. |
args |
No | {} |
Passed as --build-arg. |
build_args_from_vars |
No | [] |
Stored variables (mecca var set) to pass as build args as well as secrets. Only for values that are not secret. |
target |
No | — | Multi-stage build target stage. |
2.3 deploy[] — how to run it¶
deploy:
- name: web # required — appears in the URL
container: web # required for container services; must match containers[].name
port: 8000 # the port your process listens on INSIDE the container
replicas: 1 # 1-3
command: "gunicorn app:app --bind 0.0.0.0:8000" # optional; overrides Dockerfile CMD
health_check:
path: /health
environment:
LOG_LEVEL: info
resources:
memory: 256m
| Field | Type | Default | Bounds | Notes |
|---|---|---|---|---|
name |
string | — | required, unique | Appears in your URL. |
container |
string | — | required unless static | Must match a containers[].name. |
port |
int | none | 1–65535 | Omit ⇒ internal-only service (worker) with no URL. |
replicas |
int | 1 |
1–3 | Each replica lands on a different host. |
command |
string | Dockerfile CMD |
— | Run via sh -c. |
health_check |
block | none | — | Omit for workers. See §2.4. |
environment |
map | {} |
— | Non-secret config. See §8. |
resources.memory |
string | see §3 | — | Must carry a unit suffix. |
resources.cpu |
float | — | — | Accepted but does nothing. See §3 trap. |
drain_delay_seconds |
int | 30 |
0–300 | How long in-flight requests survive a swap. Raise for websockets/streaming. |
type |
string | none | — | static, spa. See §4. |
spa_root |
string | /app/build |
— | Only for containerised SPA services. |
singleton |
bool | false |
— | Run on one host only (e.g. a scheduler). |
visibility |
string | public |
public|private |
See §11. |
primary |
bool | auto | — | Front-door flag. See §2.6. |
drives |
list | [] |
— | Drive mounts. See §7.1. |
cache |
block | none | — | Edge caching. See §12. |
static_site |
block | none | — | Per-service static build. See §4.2. |
domain / domains |
— | — | — | Ignored. Use mecca domain add. See §13. |
2.4 health_check¶
health_check:
path: /health # default /health
interval: 30 # 5-300 seconds between checks
timeout: 5 # 2-120 seconds before a check times out
healthy_threshold: 2 # 2-10 consecutive passes to be marked healthy
unhealthy_threshold: 3 # 2-10 consecutive failures to be marked unhealthy
startup_timeout: 120 # 30-600 seconds to pass its FIRST check during a deploy
healthy_thresholdandunhealthy_thresholdboth have a minimum of2. Writing1is a hard validation error. This surprises almost everyone.startup_timeoutis the one that matters on deploy. A slow-booting app (JVM, large migrations) needs it raised, or the deploy fails while the app is still starting.- Omitting
health_checkentirely is correct for workers — a service with no port and no health check is never probed. - Your endpoint must return HTTP 200. It should be cheap and must not require authentication.
2.5 A service with no port is a worker¶
deploy:
- name: worker
container: app
command: "celery -A myapp worker -l info"
# no port, no health_check → internal only, no URL, never probed
2.6 primary — which service is the front door¶
Controls which URL the dashboard shows and screenshots. Leave it unset and AppMecca
auto-detects: every static/spa service, else the first port-bearing service whose
name is not worker*, beat*, cron-*, cron_*, _internal*.
Two traps
- Setting
primary: falseon every service yields zero front doors — no URL on the dashboard. Leave it unset instead. - A legitimate public service named e.g.
workerboardorbeatmakeris silently excluded by the auto-detect prefix match. Setprimary: trueexplicitly on it.
primary: true requires the service to be reachable (a port, or static). Otherwise it is a validation error.
2.7 static: — REMOVED, and now refused¶
The static: block no longer exists at any level. FEATURES #347 deleted the
top-level one because it did nothing: collect_command and bucket_path were
read by no code at all, so an app that declared it collected no assets and
got no error either.
⚠ BUGS #854 closed the per-service spelling (deploy[].static:), which #347
left accepted-and-silently-dropped. That gap was not hypothetical: two sample
apps declared it and one's README documented it as working, so the platform was
teaching a falsehood about itself in the repos newcomers copy from. Both
spellings are refused now.
It is now a hard refusal rather than a silent no-op, so a config carrying it will not register:
'static' at the config root was removed — it had no effect. Run collectstatic
from your Dockerfile or build command instead. `static_site:` is a different
block and is unaffected. FEATURES #347.
Collect your assets in the image instead — a RUN python manage.py
collectstatic --noinput line in the Dockerfile, or in the service's
build_command. That runs at build time, is visible in the build log, and
fails loudly when it fails.
static_site: is a different key and is unaffected
deploy[].static_site: configures a service published to S3 + CloudFront.
It is unrelated to the removed static: block and remains fully supported.
3. Resources and memory¶
Defaults when you write nothing¶
AppMecca inspects your image at deploy time and picks a default:
| Detected runtime | Default memory |
|---|---|
| JVM (Java, Kotlin, Spring) | 512m |
| Python (gunicorn/uvicorn/celery) | 256m |
| Node.js | 128m |
| Static file server (nginx) | 64m |
| Anything else / undetectable | 128m |
An explicit resources.memory always wins.
Traps¶
memory without a unit suffix is parsed as BYTES
memory: 512 means 512 bytes → a 0 MB limit. Always write 512m or 1g.
Accepted suffixes are exactly k, m, g. 512mb is not parsed — it is treated
as unparseable and silently skips the safety checks.
resources.cpu does nothing
It is accepted by the schema and validated as > 0, but no code path applies it.
Your container is not CPU-limited. Do not rely on it.
JVM apps
If your service declares a command: containing java, .jar, mvn or gradle
and memory below 512m, validation emits a warning (not an error) and the deploy
proceeds — then the JVM is killed during startup with exit code 137. If your JVM
starts from the Dockerfile ENTRYPOINT with no command: in YAML, you get no
warning at all. Set memory: 512m explicitly for any JVM service.
Containerised SPA services have a 32 MB floor
type: spa with resources.memory below 32m is a hard validation error — below
that the web server's worker process is killed silently while the container appears
healthy.
Replicas¶
replicas is 1–3. Each replica must land on a different host — this is a hard
constraint, not a preference. replicas: 3 on a three-host pool requires all three to
have capacity simultaneously, or the whole deploy fails. Start at 1.
4. The three app shapes, in full¶
4.1 Container app¶
The default. One or more Dockerfiles; every service runs as a container.
containers:
- name: web
dockerfile: Dockerfile
context: .
deploy:
- name: web
container: web
port: 8000
health_check:
path: /health
resources:
memory: 256m
4.2 Static app — no container at all¶
For anything a browser can run directly. AppMecca runs your build command, uploads the
output, and serves it from its edge cache. No Dockerfile, no containers: block.
static_site:
build_command: "npm ci && npm run build"
output_dir: "dist"
spa_fallback: true # true for SPAs, false for true static sites
index_document: "index.html"
error_document: "index.html"
deploy:
- name: web
type: static
primary: true
| Field | Default | Notes |
|---|---|---|
build_command |
npm run build |
Runs from the repo root. Must exit 0 and populate output_dir. |
output_dir |
dist |
Relative to repo root. Must not be absolute or contain ... |
spa_fallback |
false |
true for client-routed SPAs (React Router, Vue Router) so deep links work. false for Hugo/MkDocs/Jekyll where 404s should stay 404s. |
index_document |
index.html |
Served at the site root. |
error_document |
404.html |
Served on 404 when spa_fallback is false; ignored when it is true. |
flat_html_urls |
false |
true when the build emits FLAT pages (about.html) rather than about/index.html — SvelteKit adapter-static, Astro, Jekyll with pretty_urls: false. Writes the directory key too, so /about resolves. Mutually exclusive with spa_fallback (refused at validation). |
Common output_dir values: dist (Vite, SvelteKit static), build (Create React App),
out (Next.js export), public (Hugo, Jekyll), site (MkDocs).
type: static forbids container-shaped fields
A type: static service must not declare port, replicas other than 1,
drives, command, or health_check. Any of them is a hard validation error.
Converting a container service to static means deleting those keys, not just
flipping type.
SvelteKit and other adapters that emit 200.html
If your build emits 200.html instead of index.html, set
index_document: 200.html. AppMecca probes for the declared entry document and will
fail the deploy if neither is found.
4.3 Mixed app — container backend + static frontend¶
The most common full-stack shape.
containers:
- name: backend
dockerfile: backend/Dockerfile
context: backend/
deploy:
# API — runs as a container
- name: backend
container: backend
port: 8000
path: /api
health_check:
path: /api/health
resources:
memory: 256m
# SPA — built and served from the edge, no container
- name: frontend
primary: true
type: static
static_site:
build_command: "cd frontend && npm ci && npm run build"
output_dir: "frontend/dist"
spa_fallback: true
index_document: "index.html"
error_document: "index.html"
environment:
VITE_API_URL: "${service:backend}"
database:
enabled: true
The frontend's environment block is exported during its build, so ${service:backend}
resolves to the backend's public URL before the bundler runs and gets baked into the
JavaScript. This is how a static frontend learns its API address. See §8.4.
A mixed deploy produces two queue actions
Container and static halves are tracked separately but share one deployment.
mecca queue list shows both; watch both before declaring success.
4.4 type: spa — avoid it¶
type: spa is a legacy shape kept for containerised SPAs with a custom web-server
config. Writing type: spa with neither command: nor a static_site: block is a
hard validation error — the platform cannot tell whether you want a static deploy or a
container.
Unless you specifically need a custom web-server configuration, use type: static
(§4.2) or drop type: altogether.
type: is not validated against a list of allowed values
type: statik, type: Static, type: static-site are all accepted and silently
treated as ordinary container services. ssr and api are accepted and do
nothing. Only the exact lowercase strings static and spa have any effect.
5. Database¶
One line provisions a managed PostgreSQL database on first deploy.
Your container receives:
| Variable | Contents |
|---|---|
DATABASE_URL |
Full connection URL, including ?sslmode=require |
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD |
The same details, separately |
Rules¶
- Every branch gets its own database.
mainanddevnever share data. - Never hardcode the database or user name. They are opaque hash-derived identifiers (e.g.
am_a1b2c3d4e5f6_db), notmyapp_db. Always readDATABASE_URL. max_connectionsis a declaration the platform enforces at deploy admission. Set it to match your framework's real pool size (Django ≈ 1–5, SQLAlchemypool_size, HikarimaximumPoolSize). Over-declaring starves other apps; under-declaring exhausts connections at runtime.- Migrations are your job. Run them from your container's entrypoint. There is no migration hook.
- Using PostgreSQL row-level security? Set
runtime_role: true. NeverFORCE ROW LEVEL SECURITY— the platform's pre-deploy backup runspg_dumpas the table owner and fails under FORCE, which fails every deploy after the first. Withruntime_role: true,DATABASE_URL/DB_USER/DB_PASSWORDare a non-owner role that RLS binds, andDATABASE_OWNER_URL/DB_OWNER_USER/DB_OWNER_PASSWORDare the owner — run migrations with the owner URL. See Database → A Separate Runtime Role.
# Dockerfile — run migrations before starting the server
CMD ["sh", "-c", "python manage.py migrate --noinput && gunicorn app.wsgi --bind 0.0.0.0:8000"]
Node.js SELF_SIGNED_CERT_IN_CHAIN
Recent pg versions treat sslmode=require in a URL as full certificate
verification. Strip the query parameter and configure TLS in code:
Spring Boot connection exhaustion
Hikari defaults to 10 connections per replica. Set
spring.datasource.hikari.maximum-pool-size=3 and declare max_connections: 3.
6. Cache (managed Redis)¶
Your container receives REDIS_URL and CELERY_BROKER_URL (the same string), plus
CACHE_PREFIX.
Rules¶
- Always prefix your keys with
CACHE_PREFIX. The cache is shared infrastructure; the prefix is your namespace. It is injected as{app}:{branch}:. REDIS_URLandCELERY_BROKER_URLare identical. If Celery needs a distinct result backend, pointresult_backendat your database rather than Redis.CACHE_PREFIXis injected even whencache.enabledis false — do not use its presence to detect whether caching is on.
This is the internal key-value cache
For HTTP/CDN edge caching of your responses, see §12 — a completely different
feature configured under deploy[].cache.
7. Storage — drives and buckets¶
Two kinds. Picking the wrong one is the most expensive configuration mistake on the platform.
The decision, in one table¶
| Drive | Bucket | |
|---|---|---|
| Looks like | A directory in your container | An S3-API endpoint |
| Access | Ordinary file I/O (open(), fs.readFile) |
An S3 client library |
| Size ceiling | 100 GB, hard | Effectively unlimited |
| Free allowance | 5 GB per app | No allowance needed |
| Above the allowance | Needs an operator grant | Self-service |
| Relative cost | ~13× more expensive | Cheap |
| Shared across replicas | Yes | Yes |
| Shared across branches | No | No |
In mecca app backup bundles |
Yes | No |
Choose a drive when¶
- A framework demands a real filesystem path it will not let you override.
- Small, shared working state between replicas.
- You need it captured in platform backups.
Choose a bucket when¶
- User uploads, media libraries, generated images, exports, documents.
- Anything that can grow.
- Anything over a few gigabytes.
The two most costly storage mistakes
- Bulk media on a drive. You pay roughly 13× the bucket rate, hit a hard 100 GB wall, and need an operator to raise it. Put media in a bucket.
- Logs on a drive. Write to stdout/stderr instead — AppMecca collects container output automatically. A log directory on a drive is unbounded growth at the most expensive rate available.
7.1 Drives¶
Declare at app level, mount per service:
storage:
drives:
- name: media # kebab-case, starts with a letter
size_gb: 5 # 1-100, default 5
deploy:
- name: web
container: web
port: 8000
drives:
- name: media # must match a declared drive
path: /data/media # absolute path inside the container
readonly: false
pathmust be absolute and unique per service./,/etc,/usr,/var,/bin,/sbin,/bootand/devare rejected.- One drive can be mounted by several services, at different paths, with different
readonlysettings. - Every branch gets its own drive. Files uploaded on
mainare invisible ondev.
Entitlement: an app's declared drive total must fit inside the free allowance (5 GB) unless the group holds an operator grant. The ceiling is 100 GB for everyone. The group-wide total counts every app and every branch — a second branch of a 5 GB app adds another 5 GB to the group's total. Exceeding it is refused at deploy time with HTTP 409, not at commit time.
size_gb is enforced on uploads, not on your container's writes
The platform refuses an over-cap mecca drive upload, but your container writing
directly to the mount is not blocked at the filesystem level. Do not treat
size_gb as a hard quota against your own code.
7.2 Buckets¶
On deploy, AppMecca provisions each bucket and injects one scoped credential:
| Variable | Contents |
|---|---|
APPMECCA_BUCKET_UPLOADS_NAME |
Physical name of the uploads bucket |
APPMECCA_BUCKET_EXPORTS_NAME |
Physical name of the exports bucket |
APPMECCA_BUCKET_REGION |
Region the buckets live in |
APPMECCA_BUCKET_ACCESS_KEY_ID / APPMECCA_BUCKET_SECRET_ACCESS_KEY |
The credential |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_DEFAULT_REGION |
Convenience copies, so standard S3 client libraries work with no configuration |
The logical name is upper-cased with hyphens becoming underscores:
user-uploads → APPMECCA_BUCKET_USER_UPLOADS_NAME.
import boto3, os
s3 = boto3.client("s3") # credentials already in the environment
s3.upload_file("photo.jpg", os.environ["APPMECCA_BUCKET_UPLOADS_NAME"], "photo.jpg")
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({}); // credentials already in the environment
await s3.send(new PutObjectCommand({
Bucket: process.env.APPMECCA_BUCKET_UPLOADS_NAME,
Key: "photo.jpg", Body: data,
}));
Never hardcode a bucket name
The physical name is generated and differs per branch. Always read
APPMECCA_BUCKET_{NAME}_NAME.
Do not set AWS_ACCESS_KEY_ID yourself
The three convenience variables are only set if you have not set them. If you define
any of them in appmecca.yaml or via mecca var set, your value wins and every
bucket call fails with an authentication error that looks nothing like a config
mistake. Read the APPMECCA_BUCKET_* variables explicitly if you need another
credential in the same container.
Buckets are not in backup bundles
mecca app backup captures the database and drives. Bucket objects are not
included and mecca app restore will not bring them back.
7.3 Getting media in¶
Into a drive — from your machine, with the CLI:
mecca drive ls myapp media # list a directory
mecca drive tree myapp media # walk the whole drive
mecca drive du myapp media # usage against size_gb
mecca drive upload myapp media ./photo.jpg # ONE file — a directory is refused
mecca drive download myapp media path/to/file.jpg ./local.jpg
mecca drive mkdir myapp media new/folder
mecca drive mv myapp media old/path new/path
mecca drive rm myapp media path/to/file # destructive — confirms
mecca drive archive myapp media -o ./media.tar.gz # the whole drive as one file
Directory upload is recursive and resumable: files already present with a matching
size are skipped, so re-running after an interruption is cheap. --overwrite re-sends
everything. Symlinks are never followed; .DS_Store, Thumbs.db, .git, .svn, .hg
and __pycache__ are skipped. Add -b <branch> to target a non-production branch.
Into a bucket — the same verbs, where S3 semantics allow them:
mecca bucket list myapp # which buckets exist + their physical names
mecca bucket ls myapp media # one prefix level (-r for everything below)
mecca bucket stat myapp media k.jpg
mecca bucket upload myapp media ./photo.jpg photos/photo.jpg
mecca bucket download myapp media photos/photo.jpg -o ./p.jpg
mecca bucket mkdir myapp media photos/2027
mecca bucket rm myapp media photos/old.jpg # destructive — type the bucket name
mecca bucket purge myapp media # empty it; bucket survives
mecca bucket archive myapp media -o ./media.tar.gz
mecca bucket reveal myapp # show the credential once
mecca bucket rotate-key myapp # issue a new credential and redeploy
reveal is show-once — a second call is refused. Use rotate-key if you need it again.
⚠ There is no mecca bucket mv/cp. S3 has no server-side rename, so moving a
"directory" is an unbounded copy-then-delete loop with no atomicity. Use aws s3 mv
with the app's own revealed credential if you need it. Buckets also carry no quota —
unlike a drive's size_gb, nothing bounds their growth.
⚠ Bucket uploads go straight to S3 — no transit hop and no commit step, so an object is live the moment S3 accepts it. (A drive upload stages through transit storage because a filesystem cannot be presigned.)
Serving media back to users:
- Drive — your application reads the file from the mount path and serves it.
- Bucket — your application generates a pre-signed URL (recommended: no traffic through your container) or streams the object itself.
8. Environment variables¶
8.1 Where to put what¶
| Put it in | When | Visibility |
|---|---|---|
appmecca.yaml → deploy[].environment |
Non-secret config that belongs with the code | In Git, and printed in full in build logs |
mecca var set |
Secrets, per-environment values, anything you would not commit | Stored server-side; reaches the build as a secret, not stored in the image; only names appear in build logs |
Never put a credential in appmecca.yaml
Values in deploy[].environment are committed to Git and echoed verbatim into
the build log. Values set with mecca var set are never printed; only their names are.
8.2 The CLI¶
mecca var set myapp SECRET_KEY=abc123 # ALL branches (the default)
mecca var set myapp SECRET_KEY=abc123 -b dev # one branch
mecca var set myapp DB_HOST=db DB_PORT=5432 -b dev,qa # several branches
mecca var list myapp # per-branch listing
mecca var list myapp -b dev --dotenv > .env # export
mecca var load myapp .env -b dev # bulk import from a file
mecca var unset myapp SECRET_KEY -b dev
mecca var setapplies to every branch unless you narrow it with-b. The selector accepts a name (-b dev), a comma list (-b dev,qa), or a glob (-b 'feature/*').-bmust come after the subcommand — unlike-e, it is not accepted before it.- Setting is a merge, not a replace. Existing keys you do not mention are kept. To remove one, use
mecca var unset(one key per call). mecca var listprints values in cleartext — treat its output as sensitive.--dotenvrequires the selector to resolve to exactly one branch.
Changes need a redeploy
Variables are read only when a container starts. After changing one:
8.3 What AppMecca injects for you¶
Always present:
| Variable | Contents |
|---|---|
APPMECCA_ENV |
Environment name |
APPMECCA_APP |
Canonical app name (group-prefixed) |
APPMECCA_SERVICE |
This service's name |
APPMECCA_BRANCH |
Branch name, sanitised (feature/Dark-Theme → feature-dark-theme) |
APPMECCA_REGION |
Deployment region |
CACHE_PREFIX |
{app}:{branch}: — your cache namespace |
Conditionally present: database variables (§5), cache variables (§6), bucket variables (§7.2), mailer variables (§9).
8.4 Precedence — who wins¶
Built in this order, later writes overwriting earlier ones:
appmecca.yaml→deploy[].environment${...}template resolution- Platform-injected values (
DATABASE_URL,REDIS_URL,APPMECCA_*, bucket vars) mecca var setvalues- Mailer variables (§9)
Consequences:
- YAML loses to the platform. Writing
DATABASE_URL:indeploy[].environmentis silently overwritten whenever the app has a database. mecca var setbeats the platform. You can overrideDATABASE_URLorREDIS_URLwith a stored variable — useful for pointing at an external database, and a foot-gun if left behind by accident. If your app is mysteriously talking to the wrong database, checkmecca var listfirst.- Mailer variables cannot be overridden.
8.5 Cross-service references¶
- Resolves to the referenced service's public HTTPS URL.
- Only services that declare a
portare addressable.${service:worker}on a port-less worker fails the deploy. - A typo referencing an undeclared service is caught by validation.
- At build time (for static frontends) the template must be the entire value.
"${service:backend}/v1"is left literal in a build arg; append the path in your code instead.
path: — mount several services beneath ONE URL
Your app answers on one hostname, with its services on paths beneath
it. Give a service a path: and it is served there:
deploy:
- name: web
container: web
port: 3000
primary: true # the front door — serves `/`, declares NO path
- name: api
container: api
port: 8080
path: /api # served at https://<your-app-url>/api
A same-origin API needs no reverse proxy and no CORS: your frontend calls
/api/... on its own origin. Use ${service:NAME:path} if you want the
mount path itself rather than a full URL.
Four shapes are still refused, each with its own message: two front
doors (nothing can serve / twice — split the app), a type: static
service with a path: (a static origin cannot forward a path), two
services claiming /, and a mount that reaches outside its own
service.
${service:NAME:internal} — stays in-VPC, and no longer goes stale
An internal-network variant keeps the call inside the VPC instead of going out over the public route — roughly 3 ms instead of ~25 ms. It used to go stale after the callee's second deploy; that was fixed in BUGS #680, and the address a service is given is now permanent.
Both services must be in the same environment and the same branch.
8.6 Frontend variables¶
For a static frontend, deploy[].environment values are exported during the build,
so your bundler bakes them in. Use your framework's public prefix:
| Framework | Prefix |
|---|---|
| Vite | VITE_ |
| Create React App | REACT_APP_ |
| Next.js | NEXT_PUBLIC_ |
- name: frontend
type: static
static_site: { build_command: "npm ci && npm run build", output_dir: "dist" }
environment:
VITE_API_URL: "${service:backend}"
Anything baked into a browser bundle is public
Never reference a secret from a frontend service's environment.
9. Built-in email¶
AppMecca has email built in — use it instead of wiring up a mail provider.
What it is for¶
Notifying yourself: the developer, the site owner, the team that runs the app.
- A contact form was submitted — send yourself the contents.
- A threshold was crossed (stock low, signups spiked, disk filling) — send an alert.
- Something happened the owner would want to know about (new order, failed job, new registration) — send a notification.
What it is not: a bulk, marketing or customer-mailing service. Do not build newsletters, drip campaigns or mass customer email on it — the quotas (below) are sized for notification volume, and the From address is platform-stamped, not your brand.
When a user asks for "email me when the form is submitted", "notify me if X", or any send-myself-a-message feature, reach for this first — it needs no mail server, no third-party account, no credentials and no DNS. One line of YAML:
Enabling it¶
Redeploy, and your container receives APPMECCA_MAILER_URL (the complete send
endpoint — do not append a path to it), APPMECCA_MAILER_TOKEN and
APPMECCA_MAILER_FROM. Send by POSTing JSON:
import os, requests
requests.post(
os.environ["APPMECCA_MAILER_URL"],
headers={
"Authorization": f"Bearer {os.environ['APPMECCA_MAILER_TOKEN']}",
"X-AppMecca-App": os.environ["APPMECCA_APP"], # REQUIRED — 401 without it
},
json={
"to": ["user@example.com"],
"subject": "Welcome",
"text": "Plain-text body",
"html": "<p>HTML body</p>",
},
timeout=10,
)
Both headers are required on every send: the bearer token and
X-AppMecca-App set to your app's canonical name — always read it from the injected
APPMECCA_APP variable, never hardcode it.
| Field | Required | Notes |
|---|---|---|
to |
Yes | List of addresses, at least one |
subject |
No | No control characters |
text / html |
At least one | Body |
cc, bcc |
No | Lists of addresses |
reply_to |
No | Single address |
idempotency_key |
No | Prevents duplicate sends on retry |
Rules¶
- The From address is stamped by the platform (
{app}@{env-domain}) — you cannot set it. Usereply_toif replies should reach a real mailbox. - Unknown fields are ignored, not rejected — a typo in a key is silently dropped and the send still returns 200, so check your field names against the table above rather than relying on an error.
- Recipients that hard-bounce or complain are suppressed automatically; later sends to them are refused. This protects the platform's deliverability — another reason this is a notify-yourself service.
- Quotas (defaults): 2,400 emails per rolling 24 hours, per app per branch; bursts within the window are unrestricted (no per-minute limit). Ample for notifications; deliberately not sized for bulk mail. Every response carries
X-Quota-Limit/X-Quota-Remaining/X-Quota-Reset; over-quota sends return 429 with aRetry-After. - Set
idempotency_keyon sends triggered by user actions (form submissions), so a retry never emails you twice.
A typical contact-form handler:
@app.post("/contact")
def contact(form: ContactForm):
requests.post(
os.environ["APPMECCA_MAILER_URL"],
headers={
"Authorization": f"Bearer {os.environ['APPMECCA_MAILER_TOKEN']}",
"X-AppMecca-App": os.environ["APPMECCA_APP"],
},
json={
"to": [os.environ["OWNER_EMAIL"]], # yourself — set via `mecca var set`
"subject": f"Contact form: {form.name}",
"text": f"From: {form.name} <{form.email}>\n\n{form.message}",
"reply_to": form.email, # so you can reply directly
"idempotency_key": form.submission_id,
},
timeout=10,
)
return {"ok": True}
Testing it from the CLI¶
After deploying with email.enabled: true, prove the whole path works — before writing
any application code — with:
cd ~/myapp # run from inside the app's repo
# Smoke-test the relay from inside the container (must be YOUR logged-in email):
mecca app exec myapp -- sh -c 'curl -sS -X POST "$APPMECCA_MAILER_URL" -H "Authorization: Bearer $APPMECCA_MAILER_TOKEN" -H "Content-Type: application/json" -d "{\"to\":[\"you@example.com\"],\"subject\":\"test\",\"text\":\"ok\"}"'
Three things to know about how it targets the app:
- There is no app argument. The app is inferred from the directory you run it in —
the last path segment of
git remote get-url origin. Run it from the app's repo. - This assumes the repo's name is the app name. If the app was registered under a different name, detection mismatches and the command reports the app as not found.
- For an app in a shared group, prefix the root flag:
mecca --group <slug> app exec ….
It runs a send inside your live container through the injected APPMECCA_MAILER_*
variables — the real production path, not a shortcut — and the errors are actionable:
401 → the deploy with email.enabled: true hasn't landed yet; 403 → the recipient
is suppressed; 429 → quota, wait Retry-After. The recipient must be your own
logged-in address — any other is refused.
If auto-detection can't work (renamed repo, no local checkout), the manual equivalent gives explicit control over app and group:
mecca app exec myapp -- sh -c 'curl -sS \
-H "Authorization: Bearer $APPMECCA_MAILER_TOKEN" \
-H "X-AppMecca-App: $APPMECCA_APP" \
-H "Content-Type: application/json" \
-d "{\"to\": [\"you@example.com\"], \"subject\": \"test\", \"text\": \"relay works\"}" \
"$APPMECCA_MAILER_URL"'
Then observe with:
mecca email status myapp # enablement + send volume
mecca email events myapp # per-send delivery log (sent / bounced / suppressed)
mecca email suppress list myapp # who is on the suppression list
10. Cron¶
A cron job is a scheduled HTTP request AppMecca makes to one of your own endpoints — not a command run in a container.
cron:
- name: cleanup-sessions
schedule: "0 3 * * *" # 5 fields, UTC
url: /internal/cleanup # must start with /
service: web # required when the app has more than one service
method: POST # GET or POST, default POST
timeout: 30 # 5-300 seconds
- Write the endpoint yourself; guard it (it is reachable on your service's normal URL).
- Keep it fast and idempotent. If work takes longer than
timeout, have the endpoint enqueue rather than execute.
mecca cron list myapp # note: app name is POSITIONAL, there is no -a flag
mecca cron history myapp
mecca cron trigger myapp cleanup-sessions
mecca cron disable myapp cleanup-sessions
mecca cron enable myapp cleanup-sessions
11. Visibility — public and private apps¶
- public — anyone with the URL reaches your app.
- private — anonymous visitors are redirected to sign in; only members of your group are let through.
Visibility is app-wide
If any service declares visibility: private, the whole app becomes private.
You cannot mix public and private services in one app.
Flip it after registration without editing YAML:
mecca app protect myapp # → private
mecca app publish myapp # → public
mecca app protect myapp -b dev # a specific branch
Allow up to ~90 s. The ALB listener rule is rewritten within ~15 s, but each gateway replica resolves visibility from its own hostmap cache (60 s TTL) and expires independently — measured on qa, an app was still served publicly by some replicas at 62 s and fully converged at ~100 s. Until then a visitor may still reach the old behaviour; give it the full window before investigating.
⚠ If you are flipping an app private in response to a leak, the ~90 s is the number that matters — the app stays anonymously readable until every replica has refreshed. Content that was already public is what remains readable.
⚠ On a multi-branch app, pass -b. visibility is a per-branch column, so flipping
one branch leaves its siblings untouched — believing otherwise means believing an app is
protected while another branch of it is still public. Both commands refuse rather
than guess when an app has more than one branch, and name the branches so you can choose.
12. Edge caching (deploy[].cache)¶
Puts AppMecca's content delivery cache in front of a container service.
deploy:
- name: web
container: web
port: 8000
cache:
enabled: true
default_ttl: 0 # unmatched paths: 0 = do not cache
invalidate_on_deploy: true
behaviors:
- path: "/static/*"
ttl: 31536000
compress: true
- path: "/api/*"
ttl: 0
forward_all_headers: true
allowed_methods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
| Field | Default | Notes |
|---|---|---|
enabled |
true |
On as soon as the block exists. Set false to declare-but-disable. |
default_ttl |
0 |
Seconds for paths matching no behavior. 0 = no caching. |
invalidate_on_deploy |
true |
Purge the cache after each deploy. |
behaviors[].path |
— | Pattern, e.g. /static/*. |
behaviors[].ttl |
86400 |
Seconds. 0 = do not cache. |
behaviors[].compress |
true |
Compress responses. |
behaviors[].forward_query_strings |
false |
Include the query string in the cache key. |
behaviors[].forward_cookies |
none |
none, all, or whitelist. |
behaviors[].forward_all_headers |
false |
Effectively disables caching for that path. |
behaviors[].allowed_methods |
[GET, HEAD] |
Add the write methods for any /api/* path, or POST/PUT/DELETE are rejected. |
Two traps
- Writing
cache: {}under a service enables edge caching (enableddefaults totrue). To keep it off, omit the block entirely. - A cached path serves stale content until its TTL expires. Never cache authenticated responses — use
ttl: 0andforward_all_headers: truefor them.
type: static services are already served from the edge; they do not need this block.
13. Custom domains¶
domain: and domains: in appmecca.yaml are ignored. Certificates cannot be
driven from config. Use the CLI:
mecca domain add myapp www.example.com -s web
mecca domain list myapp
mecca domain verify myapp www.example.com
mecca domain remove myapp www.example.com
The command prints the DNS records to create. Your platform URL keeps working alongside the custom domain.
14. The Dockerfile contract¶
What AppMecca requires¶
- Your process must listen on
0.0.0.0(not127.0.0.1) on the port you declared indeploy[].port. - The image must run without privileged capabilities.
- If you declare a
command:in YAML, the image needs a shell (it runs viash -c).
What AppMecca does to your container at runtime¶
| Behaviour | Consequence for you |
|---|---|
Runs with all Linux capabilities dropped and no-new-privileges |
A minimal set is restored for a service on container port 80/443, and — for any other declared port — when the image runs as root and declares an EXPOSE. A service with no port: gets none. Prefer running as a non-root user with cache directories pre-created and chowned at build time, so no capability is needed at runtime. |
| Process limit of 256 | A very large worker pool or a supervisor tree can hit the ceiling. Keep worker counts modest. |
Overrides your HEALTHCHECK when the service declares health_check |
Your image must contain one of curl, wget, python3 or python for the probe to run. |
Disables your HEALTHCHECK when the service declares none |
Do not rely on the Dockerfile HEALTHCHECK as the liveness signal — the service's health_check is the real gate. |
| Builds with no layer cache | Cache-ordering tricks buy nothing. Optimise for cold-build cost: small bases, multi-stage builds, target:. |
EXPOSE is not enough
If you omit deploy[].port, AppMecca guesses from the image's exposed ports using a
preference list (8000, 8080, 3000, 5000, 80) — so EXPOSE 3000 8080 silently
selects 8080. Always declare port: explicitly.
Build arguments and secrets¶
deploy[].environment values and containers[].args are passed to docker build
as --build-arg. Declare ARG in your Dockerfile to use them.
Values set with mecca var set are passed as secrets, not build args. The build
can read them, but they are not stored in the image. Read one in the RUN step that
needs it; the id is the variable's name:
A name must be letters, digits and underscores and must not start with a digit. A variable with any other name is not passed to the build, and the build log says so.
If a stored variable is not a secret and your build needs it as an ARG (for
example a public URL baked into a frontend bundle), list it under the container:
Build args are stored in the image
Anyone who can pull the image can read every build arg from its history. Never
pass a secret as a build arg. Do not copy a secret into a file the image keeps:
RUN cat /run/secrets/X > .npmrc leaves it in that layer.
Release 6.4 transition
In release 6.4, a stored variable that your Dockerfile declares with ARG, and
that is not in build_args_from_vars, is still passed as a build arg. The build
log and mecca queue status show a warning naming it. From release 6.5 that
build is refused. Read it as a secret, or list it in build_args_from_vars.
Architecture¶
Images are built for the platform's host architecture, which may be ARM64. Do not hardcode an x86-64 binary download. Use the multi-arch variable:
Base images¶
Public images from Docker Hub, GitHub Container Registry and public registries are pulled through a platform cache automatically — no change needed. Private third-party registries are not supported; vendor what you need or publish to a supported registry.
A good Dockerfile¶
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Run as a non-root user
RUN useradd -m -u 10001 appuser && chown -R appuser /app
USER appuser
EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]
Multi-stage, building a frontend into a backend image:
FROM node:20-slim AS frontend
WORKDIR /fe
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY backend/ .
COPY --from=frontend /fe/dist ./static
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]
15. Deploying¶
First time¶
# 1. Register the repository
mecca register git@github.com:org/myapp.git
# 2. Build and deploy
mecca app deploy myapp
mecca register does not start a build. It records the app and runs a repository
pre-flight check: can AppMecca reach the repo and branch, is there an appmecca.yaml, is
it valid, do the referenced Dockerfiles exist. The app cannot build or deploy until
pre-flight passes.
| Verdict | Meaning | Fix |
|---|---|---|
ready |
Good to go | — |
pending |
Check still running | mecca app recheck myapp |
no_access |
Cannot reach the repo/branch | Grant AppMecca access to the repository; check the branch name |
needs_config |
No appmecca.yaml found |
Add one and push |
config_invalid |
The YAML is invalid | Fix it, push, then mecca app recheck myapp |
After pushing a fix: mecca app recheck myapp.
Day to day¶
mecca app redeploy myapp # rebuild from the repo and deploy
mecca app deploy myapp --no-build # redeploy the current image (fast)
mecca build trigger # queue a build; deploys automatically on success
mecca build trigger --no-deploy # build without deploying
Branches¶
Each branch is registered and deployed independently, with its own URL, database and storage.
Watching a deploy¶
mecca queue list # everything in flight
mecca queue status <action-id> # one action
mecca queue logs <action-id> # its logs
mecca logs myapp --type build # build output
mecca logs myapp --type deploy # deploy output
mecca logs myapp --type container --service web # your container's own stdout/stderr
mecca app show myapp # current state and URLs
If an action sits in queued, run mecca queue diagnose <action-id> — it explains what
is holding it (capacity, ordering behind another action, or a declared-storage refusal).
Rolling back¶
mecca build list -a myapp # Tag column; pick a SUCCEEDED build
mecca app show myapp # what is running RIGHT NOW
mecca app deploy myapp --image-tag <tag> --no-build # ship a previous image
--no-build re-uses an image that already exists, so a rollback cannot be blocked by
whatever broke the build.
How a deploy runs¶
AppMecca starts the new container alongside the old one, waits for it to pass its health check, switches traffic, then drains and removes the old container. If the new container never becomes healthy, the old one keeps serving — a failed deploy is not an outage.
This is why startup_timeout matters: it is the budget for your new container to answer
its first successful health check.
16. Restoring a database¶
From a dump you have (the usual case)¶
# 1. Create the dump — custom format is strongly recommended
pg_dump -Fc mydb > prod.dump
# 2. Restore it
# Dashboard → myapp → Database tab → choose the file → confirm
# (no CLI equivalent on the shipping `mecca` — FEATURES #366)
You are asked to confirm, then to type the app name back. The file uploads directly and the platform:
- Validates the dump (format, PostgreSQL version, content).
- Takes a safety backup of the current database — the restore is refused if this fails.
- Resets the schema and applies your dump.
- Rolls back to the safety backup automatically if the restore fails.
Then check the outcome:
Rules and limits:
| Maximum size | 5 GB |
| Accepted formats | Custom (pg_dump -Fc) recommended; plain .sql; gzipped plain .sql.gz |
| Rejected | A gzipped custom dump (.dump.gz) |
| PostgreSQL version | A dump from a newer PostgreSQL than the platform's is refused. Older → newer is fine. |
| Branch | -b defaults to main. Pass it explicitly for any other branch. |
| Authentication | Requires a signed-in session, plus the object's name typed back. API keys are refused. ⚠ The typed name is a foot-gun guard, not an auth control — the session itself is the credential. |
A plain dump taken with default flags is refused
Default pg_dump output contains ALTER ... OWNER TO, GRANT and REVOKE
statements referring to roles that do not exist on AppMecca. The platform refuses it
before touching anything and tells you to re-dump. Use either:
From a platform backup¶
mecca app backup myapp # take one now
mecca app backups myapp # list them
mecca app restore myapp <bundle-prefix> # database + drives
mecca app restore myapp <bundle-prefix> --db-only # database only
mecca app download myapp <bundle-prefix> # download the bundle
mecca app rotate-credentials myapp -b main --confirm myapp --wait # revoke the DB + cache passwords (needs a signed-in session; an API key is refused)
A bundle contains the database and a copy of each drive — not bucket objects.
Contents are checksum-verified before anything is written, and drives are swapped
atomically. On a multi-branch app, -b is required.
Seeding a new app from another app's backup¶
The seed runs once, on the first deploy.
17. Complete worked examples¶
Flask API with a database¶
containers:
- name: web
dockerfile: Dockerfile
context: .
deploy:
- name: web
container: web
port: 8000
health_check:
path: /health
resources:
memory: 256m
database:
enabled: true
max_connections: 5
Django + Celery + database + cache¶
containers:
- name: app
dockerfile: Dockerfile
context: .
deploy:
- name: web
container: app
port: 8000
command: "gunicorn myproject.wsgi --bind 0.0.0.0:8000 --workers 3"
health_check:
path: /health
startup_timeout: 300
resources:
memory: 512m
environment:
DJANGO_SETTINGS_MODULE: myproject.settings.production
- name: worker
container: app
command: "celery -A myproject worker -l info"
resources:
memory: 256m
- name: beat
container: app
command: "celery -A myproject beat -l info"
singleton: true
resources:
memory: 128m
database:
enabled: true
max_connections: 5
redis:
enabled: true
React SPA — static, no Dockerfile¶
static_site:
build_command: "npm ci && npm run build"
output_dir: "dist"
spa_fallback: true
index_document: "index.html"
error_document: "index.html"
deploy:
- name: web
type: static
primary: true
Full-stack: API container + static SPA + media bucket + drive¶
containers:
- name: api
dockerfile: backend/Dockerfile
context: backend/
deploy:
- name: api
container: api
path: /api
port: 8000
health_check:
path: /api/health
resources:
memory: 512m
drives:
- name: workdir
path: /data/work
- name: web
primary: true
type: static
static_site:
build_command: "cd frontend && npm ci && npm run build"
output_dir: "frontend/dist"
spa_fallback: true
index_document: "index.html"
error_document: "index.html"
environment:
VITE_API_URL: "${service:api}"
database:
enabled: true
max_connections: 5
redis:
enabled: true
storage:
drives:
- name: workdir
size_gb: 2
buckets:
- name: uploads # user media — object storage, not the drive
email:
enabled: true
cron:
- name: nightly-cleanup
schedule: "0 3 * * *"
url: /api/internal/cleanup
service: api
Documentation site (non-SPA static)¶
static_site:
build_command: "pip install -r requirements.txt && mkdocs build"
output_dir: "site"
spa_fallback: false # real 404s for a real static site
index_document: "index.html"
error_document: "404.html"
deploy:
- name: docs
type: static
primary: true
18. Pre-flight checklist¶
Before deploying, verify every line:
Structure
- [ ] File is
appmecca.yamlin the repository root. - [ ] Every key matches this reference exactly — no typos (§2.0).
- [ ]
deploy:has at least one service. - [ ]
containers:present unless every service istype: static. - [ ] Every
deploy[].containermatches acontainers[].name. - [ ] Every
containers[]entry writesdockerfile:explicitly.
Services
- [ ] Every web-facing service declares
port:explicitly. - [ ] Your process binds
0.0.0.0, not127.0.0.1, on that port. - [ ] Web services have a
health_checkreturning 200 at the declaredpath. - [ ]
healthy_threshold/unhealthy_thresholdare ≥ 2 if set. - [ ] Workers have no
portand nohealth_check. - [ ]
replicasis 1–3.
Static
- [ ]
type: staticservices declare noport,command,health_check,drives, orreplicas > 1. - [ ] A
static_site:block exists (top-level or per-service) for everytype: staticservice. - [ ]
output_dirmatches what the build actually writes. - [ ]
spa_fallback: truefor client-routed SPAs;falsefor true static sites. - [ ] No bare
type: spawithoutcommand:orstatic_site:.
Resources
- [ ] Every
resources.memoryhas amorgsuffix. - [ ] JVM services declare at least
512m. - [ ] No reliance on
resources.cpu.
Backing services
- [ ]
database.max_connectionsmatches the real pool size. - [ ] No hardcoded database name — code reads
DATABASE_URL. - [ ] No hardcoded bucket name — code reads
APPMECCA_BUCKET_{NAME}_NAME. - [ ] Bulk media is in a bucket, not a drive.
- [ ] Every
deploy[].drives[].namematches a declaredstorage.drives[].name. - [ ] Mount paths are absolute and not system directories.
- [ ] Cache keys are prefixed with
CACHE_PREFIX.
Secrets
- [ ] No credentials in
appmecca.yaml. - [ ] Secrets set with
mecca var set. - [ ] No secret referenced from a static/frontend service's
environment. - [ ]
AWS_ACCESS_KEY_IDand friends are not set by you if you use buckets.
Other
- [ ] No
domain:/domains:keys (ignored — usemecca domain add). - [ ] Cron
urlvalues start with/, andserviceis set on multi-service apps. - [ ] Edge
cache:blocks are intentional (the block being present turns it on).
19. Troubleshooting¶
| Symptom | Most likely cause |
|---|---|
| A setting "does nothing" | Typo in the key name — silently ignored (§2.0). Check spelling first, always. |
| Deploy fails at the health check | Your app binds 127.0.0.1, the port does not match deploy[].port, the health path 404s, or startup_timeout is too short. |
| Container starts then dies immediately | Out of memory. Raise resources.memory (§3). JVM apps need ≥ 512m. |
| Static site: home page works, deep links 404 | Prerendered pages (about.html on disk): set flat_html_urls: true (§4.2). Client-routed SPA (routes only exist in JS): set spa_fallback: true. ⚠ Do not reach for spa_fallback on a prerendered site — it returns 200 from the shell, so the links look fixed while the prerendered HTML is never served. |
| Static deploy fails looking for the entry document | output_dir is wrong, or the build emits 200.html — set index_document to match. |
Frontend calls undefined / ${service:...} literally |
The variable lacks the framework's public prefix, or the template was not the whole value (§8.5, §8.6). |
| App talks to the wrong database | A stale DATABASE_URL from mecca var set is overriding the platform's (§8.4). Run mecca var list. |
| Bucket calls fail with an auth error | You set AWS_ACCESS_KEY_ID yourself and shadowed the injected credential (§7.2). |
| Bucket variables missing entirely | Credential injection is best-effort; redeploy. Check mecca app show. |
Files uploaded on main invisible on dev |
Drives and buckets are per-branch by design (§7). |
| Deploy refused: declared drive storage | Over the free allowance / group grant (§7.1). Lower size_gb or request a grant. |
POST to a cached path is rejected |
Add the write methods to allowed_methods for that behavior (§12). |
| Serving stale content | An edge cache TTL. Never cache authenticated responses. |
| Cannot build or deploy at all | Pre-flight has not passed. mecca app show myapp, fix, mecca app recheck myapp (§15). |
Action stuck in queued |
mecca queue diagnose <action-id>. |
| Restore refused before starting | Dump taken with default pg_dump flags, or from a newer PostgreSQL (§16). |
Always start with:
mecca debug myapp # one-shot diagnostic: config, recent errors, health, URLs
mecca logs myapp # recent output
20. Quick reference¶
app_name: My App # optional display name
containers: # omit when every service is type: static
- name: web
dockerfile: Dockerfile # always write this
context: .
args: {}
target: null
deploy:
- name: web
container: web
port: 8000 # omit for workers
replicas: 1 # 1-3
command: null
singleton: false
visibility: public # public | private
primary: null # leave unset for auto-detect
drain_delay_seconds: 30 # 0-300
health_check:
path: /health
interval: 30 # 5-300
timeout: 5 # 2-120
healthy_threshold: 2 # 2-10 (minimum 2!)
unhealthy_threshold: 3 # 2-10 (minimum 2!)
startup_timeout: 120 # 30-600
resources:
memory: 256m # SUFFIX REQUIRED (k/m/g). cpu does nothing.
environment:
KEY: value # cross-service refs: "${service:<name>}" — §8.5
drives:
- name: media
path: /data/media # absolute; not /,/etc,/usr,/var,/bin,/sbin,/boot,/dev
readonly: false
cache: # EDGE caching — enabled:true by default!
enabled: true
default_ttl: 0
invalidate_on_deploy: true
behaviors:
- path: "/static/*"
ttl: 31536000
compress: true
forward_query_strings: false
forward_cookies: none
forward_all_headers: false
allowed_methods: [GET, HEAD]
static_site: # required if any service is type: static
build_command: "npm run build"
output_dir: dist
spa_fallback: false
index_document: index.html
error_document: 404.html
database:
enabled: false
max_connections: 5 # 1-50
redis: # managed Redis (ElastiCache)
enabled: false
email:
enabled: false
storage:
drives:
- name: media # kebab-case, starts with a letter
size_gb: 5 # 1-100
buckets:
- name: uploads
cron:
- name: job
schedule: "0 3 * * *" # 5 fields, UTC
url: /internal/job # must start with /
service: web # required on multi-service apps
method: POST # GET | POST
timeout: 30 # 5-300
Command cheat sheet¶
# Lifecycle
mecca register <git-url> [-b branch] # register (does NOT build)
mecca app recheck myapp # re-run repository pre-flight
mecca app deploy myapp # deploy
mecca app redeploy myapp # rebuild + deploy
mecca app deploy myapp --no-build # redeploy current image
mecca build list -a myapp # build history (Tag + Status)
mecca app undeploy myapp # stop serving
mecca app delete myapp # remove entirely
# Observe
mecca app show myapp
mecca app list
mecca debug myapp
mecca logs myapp [--type build|deploy|container] [--service web]
mecca queue list / status <id> / logs <id> / diagnose <id> / cancel <id>
mecca app stats myapp
# Variables
mecca var set/list/unset/load myapp ...
# Storage
mecca drive ls/du/tree/stat/upload/download/mkdir/mv/cp/rm/purge myapp <drive> ...
mecca bucket list/reveal/rotate-key myapp
# Database
mecca app backup/backups/restore/download myapp ... # uploading a dump: dashboard only
# Other
mecca domain add/list/verify/remove myapp ...
mecca cron list/history/trigger/enable/disable myapp ...
mecca email status/events myapp # built-in email: enablement + delivery log
mecca email suppress list myapp # suppression list
mecca app exec myapp -- <command> # one-off command in a running container
⚠ mecca app exec captures stdin whenever stdin is not a terminal — that is what makes
cat fixture.sql | mecca app exec myapp -- psql -f - work. If you are invoking it from a
harness that attaches a pipe to the CLI and never writes to it, append < /dev/null: the
CLI now refuses after 10s rather than waiting (BUGS #835), because guessing "no input" would
turn a piped restore into a silent no-op reported as success.
21. Agent operating procedure — taking a project live, end to end¶
This section is the ordered procedure an AI agent should follow when asked to put a project on AppMecca. It tells you what to inspect, what to ask the user and when, and which commands to run in which order. Do not skip the questions — several decisions (media placement, backup restore, branch) cannot be inferred from code and are expensive to change later.
Step 1 — Inspect the project's code¶
Before writing any configuration, establish these facts from the repository:
| Question | Where to look | Drives |
|---|---|---|
| Does anything run a server process? | entry points, package.json scripts, manage.py, main.go… |
App shape (§1) |
What port does it listen on, and does it bind 0.0.0.0? |
server startup code, existing Dockerfile EXPOSE |
deploy[].port, §14 |
| Is there a frontend built to static files? | vite.config.*, package.json build script, framework config |
type: static service (§4.2), its output_dir |
| Does it use a SQL database? | ORM config, DATABASE_URL reads, migration folders |
database.enabled (§5) + Step 3 questions |
| Does it use Redis / a key-value cache / Celery / Sidekiq / Bull? | broker config, REDIS_URL reads |
cache.enabled (§6) |
| Does it read or write files at runtime (uploads, media, generated files)? | upload handlers, MEDIA_ROOT, multer, file writes |
Storage (§7) + Step 3 questions |
| Are there background workers or schedulers? | Celery/Sidekiq/Bull workers, cron-like code | Port-less services (§2.5), singleton, cron (§10) |
| Does it send (or want to send) email — contact forms, alerts, notifications? | mail-library usage, SMTP config, form handlers with nowhere to send | Built-in email (§9) — use it instead of a mail provider |
| Is it a JVM app? | pom.xml, build.gradle |
resources.memory: 512m minimum (§3) |
| What secrets does it need? | .env.example, config that reads env vars |
mecca var set list for Step 5 |
| Is there an existing health endpoint? | routes | health_check.path; add one if missing |
| Is there an existing Dockerfile, and does it match §14's contract? | Dockerfile |
Fix binding/arch/user issues before first build |
Step 2 — Draft appmecca.yaml and the Dockerfile¶
- Pick the app shape with the decision tree in §1.
- Write
appmecca.yamlfrom the reference in §2 (copy the nearest worked example in §17 and adapt — do not write from memory). - Write or fix the Dockerfile per §14. Migrations belong in the container's start command (§5).
- Run the entire pre-flight checklist in §18, line by line.
Step 3 — Interview the user¶
Ask only the questions made relevant by Step 1, but never skip a relevant one. Present them together, as one short list, before touching the platform.
Always ask:
- "Which environment and which Git branch am I deploying?" (branch matters: every branch gets its own database, storage and URL — §5, §7)
- "Should the app be public (anyone with the URL) or private (your group only)?" (§11)
- "Do you have the
meccaCLI configured and signed in?" (If unsure, verify withmecca whoami. Everything below fails without it.) - "Is there a custom domain to attach, now or later?" (§13 — done after deploy, never in YAML)
If the app uses a database, also ask:
- "Is there an existing database to bring over — a backup or dump from a previous host — or do we start empty?"
- If yes: "Can you produce it as
pg_dump -Fc(custom format)? What size is it, and which PostgreSQL version produced it?" (≤ 5 GB; a dump from a newer PostgreSQL than the platform's is refused — §16) - "Should the restore land before users see the site?" (usually yes — plan restore between first deploy and announcing the URL)
If the app handles uploads/media/files, also ask:
- "Is there an existing media library to migrate (e.g. an uploads folder from the old server)? Roughly how large?"
- "Bucket or drive?" — then recommend based on §7: bucket for user uploads and anything that grows; drive only when the framework demands a filesystem path or the data must ride in platform backups. State the trade-off (drive: ~13× cost, 5 GB free / 100 GB hard cap, in backups; bucket: cheap, unbounded, not in backups) and let the user decide.
- If drive and the media is large: "Your declared
size_gbtotal above 5 GB needs an operator storage grant — do you have one, or should we use a bucket instead?"
If Step 1 found secrets, also ask:
- "Please provide values for:
<list every env var the code reads that is not platform-injected>." (Never guess secrets; never write them intoappmecca.yaml— §8.1)
If the app has forms, alerts or anything worth notifying the owner about, also ask:
- "Do you want the site to email you when things happen — form submissions, alerts, system events? Which address should notifications go to?" (If yes: enable the built-in email, §9 — no mail provider needed. Store the address with
mecca var set <app> OWNER_EMAIL=..., never hardcoded.) - If the user asks for customer/marketing/bulk email: explain the built-in email is for notifying themselves, not their customers, and that bulk mailing needs an external provider.
Step 4 — Register and pass pre-flight¶
Fix any verdict per the table in §15 (mecca app recheck <app> after pushing a fix).
Do not proceed until the verdict is ready.
Step 5 — Set environment variables before the first build¶
Secrets and per-environment values collected in Step 3:
Set them before the first deploy: variables are also delivered to the build as build
arguments, and a static frontend bakes its VITE_*/REACT_APP_*/NEXT_PUBLIC_* values
in at build time (§8.6). Remember: omitting -b writes to every branch (§8.2).
Step 6 — First deploy¶
mecca app deploy <app> [-b <branch>]
mecca queue list # find the action id(s)
mecca queue status <action-id> # watch; a mixed app has TWO actions
mecca app show <app> # URL once running
If it fails, work the troubleshooting table in §19 — health-check binding, memory, and
startup_timeout cover most first-deploy failures. A failed deploy never takes down a
previously-running version (§15).
Step 7 — Restore the database (if the user has one)¶
After the first successful deploy (the deploy also provisions the database):
# Uploading a pg_dump is DASHBOARD ONLY on the shipping CLI (FEATURES #366):
# your app -> Database tab -> choose the file -> confirm
mecca queue status <action-id> # follow the queue row
Follow §16 exactly: custom format (pg_dump -Fc) is the format that works with no extra
flags; a plain default-flags dump is refused. The platform takes a safety backup first
and rolls back automatically on failure, so this step is safe to attempt.
Note this command prompts for a typed confirmation — it cannot run fully unattended; hand it to the user or run it while they are present.
Step 8 — Migrate media (if the user has any)¶
To a drive (declared in Step 2, provisioned by the deploy in Step 6):
mecca drive upload <app> <drive> ./media/ [-b <branch>] # recursive, resumable
mecca drive du <app> <drive> # confirm usage vs size_gb
To a bucket — there is no CLI upload (§7.3). The two working patterns:
- Stage the files where the container can read them (a temporarily-declared drive, or a downloadable archive), then run an import inside the running container, which already holds the bucket credential:
- Or use the application's own upload/import feature if it has one.
Verify by exercising an app URL that serves the migrated media.
Step 9 — Final verification¶
- [ ]
mecca app show <app>— running, correct URL. - [ ] Open the URL: pages load, deep links work (static SPAs:
spa_fallback, §4.2). - [ ] Restored data visible in the app (spot-check a known record).
- [ ] Migrated media serves correctly.
- [ ]
mecca logs <app>— no error loop. - [ ]
mecca debug <app>— env-var drift report is clean. - [ ] Visibility matches the user's answer (
mecca app protect/publish <app>, §11). - [ ] If email is enabled: the relay curl in §9 delivers to your login email, and a real trigger (e.g. submitting the contact form) lands in the owner's inbox.
- [ ] If requested:
mecca domain add <app> <domain> -s <service>, give the user the DNS records it prints, thenmecca domain verify(§13).
Step 10 — Report back¶
Tell the user: the URL, which backing services were provisioned (database, cache, drives, buckets), what data was restored and how verified, which env vars were set (names only — never echo values), and anything deferred (custom domain pending DNS, media import still running, a storage grant to request).
This guide reflects the platform's current validation rules. When AppMecca's behaviour and this document disagree, the platform is right — report the discrepancy.