ADR 014: Two Distinct Registration Workflows

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:

  1. A church leader wants to register their church (create an org on the platform)
  2. 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:
    1. Leader fills in church details (name, slug, type, address, branding)
    2. Authenticates via Zitadel (new account or existing)
    3. System creates: Organization (child of default tenant’s root org) → User (ORG_ADMIN role) → OrgMembership
    4. Leader lands in the admin dashboard, ready to configure and invite members
    5. 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:
    1. User opens the church’s webapp (subdomain) or native app
    2. Authenticates via Zitadel (new account or existing)
    3. Auto-provisioned in the tenant → checks org’s registrationMode:
      • open: auto-join, OrgMembership created
      • invite_only: rejected with “contact your administrator”
      • by_request: request submitted, admin approves
    4. 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:
    1. Superadmin creates new Tenant with root org
    2. Existing orgs from default tenant can be moved into the new tenant
    3. New orgs can be created within the new tenant
    4. 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:

SurfaceHow org is determined
WebappSubdomain: {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

AspectChurch RegistrationUser Registration
SurfaceBackend (/register)Webapp / Native app
AuthenticationOIDC (session-based)OIDC (token-based)
CreatesOrganization + User + OrgMembershipUser + OrgMembership
User roleORG_ADMINmember
Org contextCreated in the flowImplicit from subdomain/app
Data collectedChurch name, slug, type, addressNone (auto-provisioned)

Consequences

  • The backend /register creates 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)