Relay v3.5 · concept · 2026-08-17

Org onboarding & where your bytes live

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.

1Identity
2People
3Placement
✓Review

Name the organization

This is the trust, billing and residency boundary — not a folder. Everything below it inherits what you decide here.

Seeds the onboarding track and the screen-gating preset — a default an admin can always override, never a fork. If it can't do that, it shouldn't be asked.
Shows on the org detail surface and in the switcher. Needs orgs.description — not in the schema today.

Who's in it

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.

Offer this org to anyone at 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.

This mechanism is already built end to end — org_autojoin_rules, request queue, admin approve/deny, in-app + email notification on all four events. It has simply never been offered at creation.
Each accepted invite is a seat, and seats are what the Team quota multiplies — watch the card as you add.

Where it lives

Two decisions: the first Team inside the org, and the jurisdiction every byte this org stores will physically sit in.

Every org gets an implicit default Team and Project so it is usable with zero setup; naming one here just means you don't inherit "Default". Projects inside it are unlimited.
Pre-answered from your country signal. Change it now if it's wrong — it freezes on the first object this org stores.
Why this one is different. Every other answer on these three screens is editable forever. This one isn't: moving regions means copying existing objects and re-pointing them, so the pattern is derive → show → allow change → freeze on first write. Asking late is how an EU customer's bytes end up in Ohio.

Create Acme Research

One write: the org, its default Team and Project, the owner membership, the domain claim, and the pending invites.

Org
Acme Research
Context continuity across the research team
Purpose
Work projects
Seeds the onboarding track — overridable.
Domain
acme.dev
Requests go to an admin queue. Never an automatic membership.
People
You + 0 invited
First Team
Research
Residency
US / Canada — us-ca
◆ Locks on first write

Created

What the wizard actually wrote, and which of it exists in the schema today.

WrittenWhereStatus today
org + default workspace + owner membershipcreate_org()Live
domain claimadd_org_domain()Live
email invitescreate_invite()Live
purpose · descriptionorgs.purpose / .descriptionNot in schema
data_regionorgs.data_regionNot in schema
The flow is buildable behind a flag that is already off. Self-serve team-org creation is a hard kill-switch — 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.
What this deliberately does not copy. The flow it is modelled on puts the region on the project. Relay puts it on the org — the org is the trust · billing · residency boundary, and per-Project regions would let a single org straddle jurisdictions, which breaks the one-owner-one-jurisdiction claim the trust ladder is sold on. Projects inherit; they never choose.

One question, no wizard

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.

Pre-answered from your country. Editable until your first upload, then frozen — same rule as the team plane, because it is the same column.
Required — all plans
There is no "not now". Ratified 2026-08-17: residency selection is required on every plan — not an Enterprise feature, and not a card that can be dismissed forever. An account that reaches its first upload without an answer on record is the failure this exists to prevent, so the question is asked until it is answered.

On the account lens

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.

Handle
@erik
Email
erik@relayctx.com
Avatar
Set
Data lives in
US / Canada — us-ca
Everything this account stores — avatar, attachments, exports.
Three facts, never one. The surface renders {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.

Where the question lands

Three candidate moments, in preference order. All three can ship; the open decision is which is required.

01
At signup

If a country signal exists. Pre-answered, confirmable, one line. Cheapest for the user, strongest for compliance.

02
Settings → Data

Always present, always readable. Editable until the first write, then frozen with a migrate-by-support affordance.

03
Before the first upload

The backstop. Never let the first byte land before the question has been asked at least once.

What is broken right now

Not a hypothetical — this is the live behaviour.

Every avatar uploaded today lands in the US bucket, regardless of who uploaded it. Not through a bug: 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.
The layer underneath is correct. 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.
How this gates — ratified 2026-08-17

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.

CapabilityPlanWhy
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.
Selling where the default bucket is is indefensible. Selling the contract around it is the enterprise product. Consistent with the locked "feature-gated free, no item-count penalty" principle.
Required makes provisioning a blocker, not a nice-to-have. A required choice that offers a region with no bucket behind it produces a fail-closed 503 on that user's first upload — worse than not offering it at all. Verify storage.configured_regions() against production before shipping the requirement; if eu is not ready, provision it or do not present it.
On the copy, precisely. GDPR does not literally require EU personal data to stay in the EU — it requires a lawful transfer mechanism (adequacy or SCCs) for transfers out. Residency is the better of the two routes here, and it is what the trust ladder already promises. But the UI should say "choose where your data is stored", never "required by GDPR" — the second is a legal claim stated inaccurately, on the one screen where precision is the product.
Concept — proposal, not canon. Nothing here is built.
Plan of record: relay-board/docs/product/org-management-program-map.md
Specs: relay-platform/docs/DESIGN-NOTE.data-residency.md · SPEC.org-team-project.md · SPEC.org-plan-model.md · DESIGN-NOTE.autojoin.md
Siblings: app-v35/roles-org.html (org RBAC) · console-v35/org-ia.html (governance) · app-v35/welcome.html (first run)