Org creation is the only moment we can ask irreversible questions. Relay already has every concept a good setup flow collects — identity, purpose, teammates, domain autojoin, a first container, a storage region. It collects one of them (name), in a one-field modal, and the one that is genuinely hard to reverse — where the bytes live — it never asks at all.
The two asks that produced this page look separate: a wizard for team orgs, a storage
choice for personal profiles. They are the same column — orgs.data_region — surfaced
twice, with different ceremony. That is the claim, so both planes are drawn here behind one switch.
This is the trust, billing and residency boundary — not a folder. Everything below it inherits what you decide here.
orgs.description — not in the schema today.A workspace with one member never becomes a workspace. Claiming your domain is the strong move; individual invites are the fallback for domains you can't claim.
acme.dev
New signups on your domain are offered the org and can request to join — an admin approves each one. Never silent, and declining leaves them on their personal org for good. Public providers (gmail, yahoo…) are rejected, and a domain already claimed elsewhere returns a 409.
org_autojoin_rules, request queue, admin approve/deny, in-app + email
notification on all four events. It has simply never been offered at creation.Two decisions: the first Team inside the org, and the jurisdiction every byte this org stores will physically sit in.
"Default".
Projects inside it are unlimited.One write: the org, its default Team and Project, the owner membership, the domain claim, and the pending invites.
What the wizard actually wrote, and which of it exists in the schema today.
| Written | Where | Status today |
|---|---|---|
| org + default workspace + owner membership | create_org() | Live |
| domain claim | add_org_domain() | Live |
| email invites | create_invite() | Live |
| purpose · description | orgs.purpose / .description | Not in schema |
| data_region | orgs.data_region | Not in schema |
RELAY_SELF_SERVE_ORG_CREATE defaults off and returns
403 {feature_unavailable:true} for everyone. So this can be built, merged and
rehearsed without turning teams on; flipping the flag stays a separate, deliberate call.
A personal org is auto-created at signup with a canonical name
personal:{user_id}. There is nothing to name and nobody to invite — it is
single-member forever, enforced by Postgres triggers, not by convention. Exactly one
question is worth asking.
Residency sits beside identity, not buried in a settings page — where your data lives is part of what your account is, at the same standing as your handle, email and avatar.
{region, confirmed, ready, locked} — never a bare region string.
confirmed separates a defaulted us-ca from a chosen one; they are
the same string and not the same state, and only the second one means somebody
decided. ready says whether that region has a provisioned backend — showing a
region without it would be lying by omission, because the user's next upload would
fail closed. Shipped as the residency block on
GET /api/user/v1/profile.
Three candidate moments, in preference order. All three can ship; the open decision is which is required.
If a country signal exists. Pre-answered, confirmable, one line. Cheapest for the user, strongest for compliance.
Always present, always readable. Editable until the first write, then frozen with a migrate-by-support affordance.
The backstop. Never let the first byte land before the question has been asked at least once.
Not a hypothetical — this is the live behaviour.
db.update_user_avatar(user_id, b64, mime, region=None) accepts
a region, and its one caller passes nothing. A parameter was prepared and never filled.
STORAGE_REGIONS,
region_for_country() and get_storage(region) are all built and
fail closed — an unconfigured region returns a not-ready backend rather than quietly
writing to another jurisdiction's bucket. The missing piece is a column and a routing call,
not a design.
Residency selection is required on every plan.
The feature register had gated Data residency (orgs.data_region) — Org —
Enterprise, and that could not survive the personal plane above: if choosing a region were
Enterprise-only, every Free and Pro user in the EU would have their profile bytes written to a
US bucket — the exact breach the fail-closed storage layer was built to make structurally
impossible.
| Capability | Plan | Why |
|---|---|---|
| Selection — choose your region at setup; writes honour it; fail-closed | All plans — required | A placement choice, not a feature. Costs nothing per-user to honour and is load-bearing for the trust ladder's credibility at every rung, not just the top one. Required rather than merely available, so no account reaches first-write without an answer on record. |
| Guarantees — contractual pinning, DPA, region attestation in audit export, region-restricted or custom regions, in-tenancy | Enterprise | This is the paid governance thesis and Trust Ladder L3. Unchanged. |
storage.configured_regions() against production
before shipping the requirement; if eu is not ready, provision it or do not present it.
relay-board/docs/product/org-management-program-map.mdrelay-platform/docs/DESIGN-NOTE.data-residency.md ·
SPEC.org-team-project.md · SPEC.org-plan-model.md ·
DESIGN-NOTE.autojoin.mdapp-v35/roles-org.html (org RBAC) ·
console-v35/org-ia.html (governance) · app-v35/welcome.html (first run)