ADR 014: Two Distinct Registration Workflows
Status
Accepted (updated 2026-03-12: “tenant registration” creates org in default tenant; denomination registration creates new tenant)
Date
2026-03-11
Context
The platform serves two fundamentally different registration needs:
- A church leader wants to register their church (create an org on the platform)
- A church member wants to join an existing church (sign up within their church’s app/subdomain)
A third workflow exists for denominations that want their own tenant (custom domain, white-label app), but this is a superadmin/sales-assisted operation, not a self-service flow.
Decision
Design Principle: Two self-service workflows, always org-aware
Workflow 1: Church Registration (backend only)
- Who: Church leaders registering their church on the platform
- Where: Symfony backend at
/register— server-rendered form (Twig) - Not available in: the webapp or native app
- Flow:
- Leader fills in church details (name, slug, type, address, branding)
- Authenticates via Zitadel (new account or existing)
- System creates: Organization (child of default tenant’s root org) → User (ORG_ADMIN role) → OrgMembership
- Leader lands in the admin dashboard, ready to configure and invite members
- Church is immediately accessible at
{slug}.churchapp.io
- Key characteristic: Creates an org in the default tenant. No new tenant is created. The church uses the standard app out of the box.
Workflow 2: User Registration (webapp / native app only)
- Who: Members joining an existing church’s community
- Where: React Native webapp (via subdomain) or native app — never in the backend
- Always in org context: The org is determined by the subdomain (
icf-zurich.churchapp.io) or org selection in the native app. - Flow:
- User opens the church’s webapp (subdomain) or native app
- Authenticates via Zitadel (new account or existing)
- Auto-provisioned in the tenant → checks org’s
registrationMode:open: auto-join, OrgMembership createdinvite_only: rejected with “contact your administrator”by_request: request submitted, admin approves
- If already a member → authenticated, existing User loaded
- Key characteristic: The org is implicit from context. The user never picks a tenant.
Workflow 3: Denomination Tenant Creation (superadmin / sales-assisted)
- Who: Denomination leaders who want custom branding, custom domain, own native app
- Where: Superadmin panel or assisted setup
- Flow:
- Superadmin creates new Tenant with root org
- Existing orgs from default tenant can be moved into the new tenant
- New orgs can be created within the new tenant
- Custom domain and white-label app configured
- Key characteristic: This is a platform-level operation, not self-service (yet).
The org context rule
In the user-facing app (webapp + native), an organization is always present:
| Surface | How org is determined |
|---|---|
| Webapp | Subdomain: {slug}.churchapp.io → org resolve → gets orgId + tenantId |
| Native app (denomination) | Org picker within hardcoded tenant |
| Native app (platform) | Org picker (all orgs user belongs to, across tenants) |
Separation of concerns
| Aspect | Church Registration | User Registration |
|---|---|---|
| Surface | Backend (/register) | Webapp / Native app |
| Authentication | OIDC (session-based) | OIDC (token-based) |
| Creates | Organization + User + OrgMembership | User + OrgMembership |
| User role | ORG_ADMIN | member |
| Org context | Created in the flow | Implicit from subdomain/app |
| Data collected | Church name, slug, type, address | None (auto-provisioned) |
Consequences
- The backend
/registercreates an org in the default tenant, not a new tenant - The webapp/native app never creates orgs — it only handles joining
- New OIDC users are auto-provisioned in the tenant, but org membership depends on
registrationMode - The frontend resolves an org (not tenant) from the subdomain
- Tenant creation is a superadmin operation, not self-service (for now)