# Apps & domains Apps are tenants; domains route hosts to apps. See [Core concepts](../../guide/concepts.md#apps). ## Apps | Method & path | Capability | Notes | |---|---|---| | `GET /api/v1/admin/apps` | authenticated | lists apps the caller can see (members: only theirs) | | `POST /api/v1/admin/apps` | `InstanceCreateApp` (owner/admin) | create | | `GET /api/v1/admin/apps/{id_or_slug}` | `AppRead` | one app | | `PATCH /api/v1/admin/apps/{id_or_slug}` | `AppAdmin` | update name/description/slug | | `DELETE /api/v1/admin/apps/{id_or_slug}` | `AppAdmin` | delete (cascades scripts, routes, data) | | `POST /api/v1/admin/apps/{id_or_slug}/slug:check` | `AppAdmin` | `{"new_slug":"…"}` → `{ok, conflict_kind?, current_app?, reason?}` | Create: ```sh curl -X POST $PICLOUD/api/v1/admin/apps -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"slug":"demo","name":"Demo App","description":"…"}' ``` ```json {"id":"41f42060-…","slug":"demo","name":"Demo App","description":null, "created_at":"2026-…Z","updated_at":"2026-…Z"} ``` **Slug rules:** `^[a-z0-9][a-z0-9-]{0,62}$`, and not one of the reserved words (`new`, `api`, `admin`, `apps`, `login`, `healthz`, `version`, …). Renaming a slug records the old one for redirects; reclaim a released slug with `"force_takeover": true` in the create/patch body. ## Domains A non-`default` app's routes only match once the app **claims** the request `Host`. | Method & path | Capability | |---|---| | `GET /api/v1/admin/apps/{id_or_slug}/domains` | `AppRead` | | `POST /api/v1/admin/apps/{id_or_slug}/domains` | `AppManageDomains` | | `DELETE /api/v1/admin/apps/{id_or_slug}/domains/{domain_id}` | `AppManageDomains` | ```sh curl -X POST $PICLOUD/api/v1/admin/apps/demo/domains -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' -d '{"pattern":"demo.localhost"}' ``` ```json {"id":"6ff04d56-…","app_id":"41f42060-…","pattern":"demo.localhost", "shape":"exact","shape_key":"exact:demo.localhost","created_at":"2026-…Z"} ``` **Pattern shapes** (use `{name}`, never `:name`, in domains): - exact — `app.example.com` - wildcard — `*.example.com` - parameterized — `{tenant}.example.com` (the segment is captured into a route param) The most specific claim wins. Claiming a host another app already holds (in the same shape) returns `409`. The `default` app ships claiming `localhost`. Locally, claim `something.localhost` and test with `curl -H 'Host: something.localhost' $PICLOUD/...`. **CLI:** `pic apps create demo --name "Demo App"`, `pic apps domains add demo demo.localhost`, `pic apps ls`, `pic apps show demo`.