"Tutorial: publish an app"
The app-owner journey end to end, from self-serve registration through the dev group, subdomain and external-resource pins, to handing it to a publisher.
You have built something and want it in the Atrium directory. This walks the whole path in order, naming the exact page or route at each step.
Registration is open to everyone. The gate sits one step later, at rollout, so nothing here needs an admin until the final step.
1. Register
Three doors, one service and one set of rules:
| Door | Where |
|---|---|
| UI | /admin → Register app |
| API | POST /api/apps/register with { id, name, description } |
| Agent / MCP | the register_app tool |
The API door authenticates any signed-in user or any API key. There is no role check on registering.
id is your slug: lowercase alphanumerics and dashes, up to 80 characters. It is
the handle for everything downstream, including your subdomain, so choose it
deliberately.
What the server forces, regardless of what you send
This envelope is applied server-side. A client cannot opt out of it by passing extra fields:
| Field | Forced to | Why |
|---|---|---|
visibility | SECRET | The app appears in your launcher and nobody else's |
syncZitadel | false | No SSO project is created, so nothing lands in the org IdP unreviewed |
provisionDevGroup | true | You get a working dev group immediately |
| connector toggles | llm-gateway on; observability, tracing, source-control off | The LLM gateway needs no prerequisite from you; the others each need one you have not supplied yet |
The toggles are per capability, not per vendor — observability,
tracing, llm-gateway, source-control are the ones an app can be opted out
of in the UI. Which provider fills a capability is org-level configuration, so
swapping one observability provider for another does not reset every app's
toggle.
That is why open registration is safe: a fresh app is invisible, holds no identity project, and grants nobody anything. Blast radius is zero until someone approves it.
You come out of this step with the app row, an owner row naming you, a dev group, and, when a DNS connector is connected, a best-effort auto-claimed subdomain. The claim is first-wins and is skipped silently if the name is already taken, so verify it rather than assuming.
2. Know what you can and cannot change
You are now the app's owner, a per-app role held in Atrium's own tables rather than a Zitadel role.
| You can | You cannot |
|---|---|
| Edit name, description, environment URLs, contact, where-hosted | Change category or card visibility |
| Delete the app you own | Publish it yourself |
Create your own PROJECT / AUXILIARY groups | Create a DEPARTMENT group |
| Manage your subdomains | Re-map the Zitadel project |
| Mint personal API keys | Toggle public access |
Edit at /admin/apps/<slug>. The PATCH route behind that page accepts a bag of
fields and silently drops anything above your tier, returning them in
droppedFields. So a request that appears to succeed may not have applied
everything you sent; check that list rather than assuming.
The same rule applies through the agent: update_app filters the patch by field
tier and refuses outright if nothing survives.
3. Understand your dev group
Registration provisions a dev group, which is the thing that projects your app into the surrounding tools. What matters is not which tools, but that a sync target runs in one of two modes, and the chips on the group card say which:
- A live chip means "belong to this group and you have access there, now" — the membership change is written immediately.
- A preview chip means "this app is wired here, but provisioning is a separate deliberate step, not driven by each member add". Real writes happen at app-create or a deliberate backfill.
Do not read a preview chip as working access. That is the whole point of the distinction: an infrastructure provisioner that only previews would otherwise look exactly like access you already have.
Today's targets, as an example of how the two modes land:
| Target | Mode |
|---|---|
| LiteLLM, Sentry, Langfuse | live |
| Bitbucket, Dokploy | preview |
4. Claim a subdomain
If auto-claim did not fire, or you want a different name, claim one explicitly.
- UI: the DNS section of
/admin/apps/<slug> - Agent:
claim_dns_subdomain, withget_dns_targetsto see the zones and open namespace first, andlist_dns_recordsto see what you already hold
Claims are first-wins and records Atrium does not manage are hard-protected. The full model is on DNS and subdomains.
5. Pin existing resources instead of minting duplicates
This is the step most people miss, and it is the one that prevents a mess.
If your app already has a team, a project or an org at one of these vendors that predates Atrium, pin it. The reconciler then adopts the existing resource, matching it and maintaining permissions, instead of minting a near-duplicate alongside it.
What you may pin is a resource kind — the vendor-side noun a connector owns
(team, project, org). An imported connector declares its kinds in the
manifest's resources block, including whether a kind is minted once per app
or once per stack. Today's built-in kinds:
| Target | Kinds you can pin |
|---|---|
| LiteLLM | team |
| Sentry | team, project (per-stack) |
| Langfuse | org |
| Bitbucket | project |
| Dokploy | project |
The Sentry project pin is the only per-stack one today; everything else is stack-less.
Discover what is adoptable with discover_external_resources, then pin with
set_external_link, inspect with list_external_links, and remove with
delete_external_link. Atrium also records provenance when a reconciler
creates a resource, so teardown knows what it minted versus what it adopted.
One honesty note: these four tools sit in the admin tier, so as a plain app owner you will need an admin to run the pin for you. Do it before the first reconcile if you can, because adopting after a duplicate exists is more work than adopting before.
6. Hand it to a publisher
You cannot publish your own app. Ask an app publisher, whose entire reason for existing is that approvals should not queue behind the handful of instance admins.
Publishing flips visibility from SECRET to DISCOVERABLE. It does not hand
the publisher ownership, and it does not change what your app is: category and
public access stay admin-only precisely so approving is not the same as
rewriting.
The publisher's side of this, including how the role is granted through a group so approver rights track the org chart, is on Publishing apps.
Checklist
- Registered, slug chosen deliberately
- Basics filled in at
/admin/apps/<slug>,droppedFieldschecked - Dev-group sync targets understood, live versus preview
- Subdomain claimed and verified
- Pre-existing external resources pinned, not duplicated
- Sent to a publisher for rollout