Projects and environments
Two organizing concepts above the App level:
- Project — a collection of related apps owned by an Organization.
Example: org
acmehas projectstorefrontcontaining appsweb,api,worker. - Environment — a deployment slot inside a project. Each project has at
least one environment (
production, auto-created); add more for staging, preview, etc.
Apps belong to a single Project. The same App deploys into one or more of the project's Environments. Each (App, Environment) pair has its own env vars, domains, managed databases, scale settings, deployments, and live containers.
Hierarchy
Organization (acme)
└── Project (storefront)
├── Environment (production) ← auto-created, BranchPattern "main"
├── Environment (staging) ← optional, BranchPattern "staging"
└── App (web)
├── per-env: env vars, domains, databases, scale, deployments
└── one git remote (shared across envs)
Branch-based deploys
When you push to a branch, the API queues a deploy in every Environment
whose BranchPattern matches:
| Push to | With these envs | Triggers deploys to |
|---|---|---|
main |
production (main) |
production |
staging |
production (main), staging (staging) |
staging |
feature/xyz |
production (main), staging (staging), preview (*) |
preview |
feature/xyz |
production (main) |
(skipped) |
A specific match (main) wins over the catch-all *. If no env matches, the
push is accepted but no deploy is queued — the API returns { skipped: true, reason: "..." } and the git client sees [paas] skipped: ....
Auto-assigned hostnames
Every (App, Environment) gets an apex hostname:
{org-slug}.{project-slug}-{app-slug}-{env-slug}.{wildcard-base}
For org acme, project storefront, app web, env production, with
PAAS_WILDCARD_BASE=apps.example.com:
acme.storefront-web-production.apps.example.com
acme.storefront-web-staging.apps.example.com
The apex is HTTP-only by default. Let's Encrypt's HTTP-01 challenge can't issue wildcard certs for
*.apps.example.com, so the apex stays plain HTTP. Custom domains do get TLS — see below.
What's per-environment vs shared
| Aspect | Scope | Notes |
|---|---|---|
| Code / Dockerfile | App | One git remote, shared across envs. |
| Container port | App | Same port everywhere. |
| Env vars | (App, Env) | Set DATABASE_URL, API_KEY, LOG_LEVEL differently per env. |
| Domains | (App, Env) | Production at your-domain.com, staging at staging.your-domain.com. |
| Managed Postgres | (App, Env) | Different DB instance per env — staging cannot see prod data. |
| Scale (replicas, CPU, memory) | (App, Env) | Run prod with 3 replicas / 1GB; staging with 1 replica / 256MB. |
| Deployments | (App, Env) | Each env has its own deployment history. |
CRUD via API
# List projects in an org
curl -H "Authorization: Bearer $TOKEN" \
https://paas.example.com/api/orgs/acme/projects/
# Create a project (Production env auto-created)
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"slug":"storefront","displayName":"Storefront"}' \
https://paas.example.com/api/orgs/acme/projects/
# Add a staging env
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"slug":"staging","displayName":"Staging","branchPattern":"staging","makeDefault":false}' \
https://paas.example.com/api/orgs/acme/projects/storefront/envs/
# Create an app — bound to all current envs automatically
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"slug":"web","displayName":"Web","containerPort":8080}' \
https://paas.example.com/api/orgs/acme/projects/storefront/apps/
# Set an env var, only on staging
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"value":"true"}' \
https://paas.example.com/api/orgs/acme/projects/storefront/apps/web/envs/staging/env-vars/DATABASE_DEBUG
# Scale just production
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"replicas":3,"cpuMillis":1000,"memoryMb":512}' \
https://paas.example.com/api/orgs/acme/projects/storefront/apps/web/envs/production/scale
CRUD via dashboard
/orgs → click an org → /projects?org=… → click a project → /apps?org=…&project=… →
add envs / create apps. App detail page (/app?org=…&project=…&slug=…) has
an env tab bar — switching tabs shows that environment's deployments,
env-vars, domains, databases, and live logs.
Deleting
- Cannot delete the default environment of a project — change which env
is default first (
PATCH /envs/{env}withmakeDefault:trueon another). - Deleting an environment removes its env-vars, domains, databases, and containers. The app's git repo and other envs are unaffected.
- Deleting a project soft-deletes it (it stops accepting pushes / new deploys but is recoverable until you hard-purge from the DB).