# Onboarding a new Provider This is a config + DNS exercise, not a code change. The V3 schema already supports it (see [tenancy.md](./tenancy.md)). ## Inputs Before starting, gather: - `slug` — short, kebab-case, globally unique (`merche-biche`, `transquinnftw`, `cocotte`) - Operating mode: **Person-only** (standalone Provider), **Person + owns Org** (agency-shaped), or **Person joins existing Org** (managed talent under someone else's agency) - Domains: their `{slug}.com` if going generic, or a brand domain if applicable - Brand assets: site copy, logo, color palette → fed into `org-site` config if Org-shaped - Email / SMTP plan: do they want a mailbox on their `.com` (defensive-coms pattern) or just inbound forwarding? ## Database steps Run on the authoritative Postgres host (see [phase-5-gates.md §5.4](./phase-5-gates.md) for which host that is): ```sql -- 1. Create the Person tenant INSERT INTO users (id, slug, email, display_name) VALUES (gen_random_uuid(), 'merche-biche', 'merche@example.com', 'Merche Biche') RETURNING id; -- Capture the returned UUID as $merche_id -- 2a. If Person-only: stop here. -- 2b. If they own a new Org: INSERT INTO orgs (slug, name, owner_id) VALUES ('merche-collective', 'Merche Collective', '$merche_id'); INSERT INTO org_members (org_id, user_id, role) VALUES ((SELECT id FROM orgs WHERE slug='merche-collective'), '$merche_id', 'owner'); -- 2c. If they join an existing Org (e.g. Cocotte adds them as talent): INSERT INTO org_members (org_id, user_id, role) VALUES ((SELECT id FROM orgs WHERE slug='cocotte'), '$merche_id', 'member'); ``` No schema migration. No code change. The existing tables accept the new rows. ## DNS For each domain the Provider needs (most start with one or two): - A record → vps-0 (`89.127.233.145` today); if the Provider warrants their own VPS later, point at that - AAAA record → vps-0 IPv6 if/when available - If hosting mail on the domain, the usual MX / SPF / DKIM / DMARC trio. See `lilith-platform.live/deployments/@domains/quinn.www/scripts/agency-brands.conf` for the canonical brand-mail pattern. ## Deploy config Templated, not forked. For each Provider domain that needs a build/serve target: ```bash cd @platform/deployments/@domains/ cp -r _template/{provider}.com {newprovider}.com # Edit services.yaml: domain, feature, aliases, deploy_state.held_back: true # Edit deploy.sh paths (REMOTE_DEST, REMOTE_NGINX_CONF) # Edit nginx/prod.conf: server_name + root path ``` Then once the domain points at vps-0 and the webroot exists: ```bash ./run deploy:{newprovider} --from-local --nginx # HTTP stub ssh quinn-vps 'certbot --nginx -d {domain} -d www.{domain} --non-interactive --agree-tos -m --redirect' # Sync the certbot-modified conf back to the repo so future deploys preserve TLS: ssh quinn-vps "cat /etc/nginx/sites-available/{newprovider}" \ | ssh apricot "cat > ~/Code/@projects/@atlilith/@platform/deployments/@domains/{newprovider}.com/nginx/prod.conf" # Flip held_back: false in services.yaml ``` This is the same pattern as the cocotte/sansonnet cutover on 2026-05-17 (see `lilith-platform.live` memory `project_cocotte_sansonnet_prod_ready`). ## Context switcher If the new Provider is part of an Org Quinn already owns, no SSO change — the org context switcher in `provider-portal` will see them as soon as the `org_members` row lands. If the new Provider is a fully independent Person (their own account), they go through normal signup → SSO issues a JWT with no `org_id`. They can later create or join Orgs from their dashboard. ## What you do NOT do - Don't fork the codebase. Provider customization is config, not source. - Don't write a new `merche-*` feature or app. Internal package names are provider-generic ([naming.md](./naming.md)). - Don't grant them write access to the platform repo. Provider-side custom work, if any, goes in their own `deployments/@domains//` subtree. - Don't backfill historical data into their tenant. New Provider = new rows from now on. ## Success check - `provider-portal` at `{newprovider}.my` (or `quinn.my` if they're a sub-account in an Org Quinn owns) loads with their identity - A test booking / inbox message / analytics event lands with the correct `user_id` (and `org_id` if applicable) and is invisible to other tenants - RLS catches an intentional bad query (try to read a different tenant's row directly — should return 0 rows)