Single-user local Coral, the default, has no membership at all: the local
process is treated as the owner of every workspace on the machine. Everything
below applies to a server started with login enabled.
Signing in is not access
Authentication and membership are separate. Authentication answers who are you; membership answers what may you reach. Coral keeps them apart on purpose, and a successful sign-in settles only the first. A verified login writes one row in the user directory and refreshes it on each subsequent sign-in. It creates no workspace, selects no workspace, and grants access to none. A person who has signed in and holds no membership sees an empty workspace list, and every workspace on the server reads to them as not found. Reef reports this asNo Coral workspace is configured. rather than as a
permission error, because from that caller’s position there is nothing there.
There are exactly two ways to hold a membership:
- Create a workspace. The creation grants its creator ownership in the same transaction. Any signed-in person may do this.
- Be added to one by an owner.
Owner and Member
Coral has two levels of workspace access, not one per operation: read what is in the workspace, or change the workspace and who may reach it.
A few of these are worth stating outright:
- Every source operation is owner-only, the read-only ones included. Source responses carry source configuration and credential metadata, so there is no member-visible source view to expose yet. Members can query through a source without being able to see how it is configured.
- Traces are owner-only because they replay what other callers ran, including their SQL.
- Changing the function set is owner-only because an installed table function is SQL that every member of the workspace then runs.
- A workspace always keeps at least one owner. Removing or demoting the last one is refused rather than performed.
- Reading the deployment’s user directory requires owning a workspace. The directory exists so an owner can name somebody as a member; a caller who owns nothing has nobody to name. This refusal is plain rather than concealing, because the directory is deployment-wide and denying it hides no particular person.
- Runtime feature flags are host-global, not workspace-scoped, so there is no workspace whose owner could be entitled to manage them. Any signed-in caller can read their status, but a server with login enabled lets nobody change them: flag changes are reserved to the built-in local principal, which a login-enabled server does not admit.
Your MCP token is your full authority
The token your MCP client holds is minted by the shared-server login and authenticates you — the signed-in user, at whatever role your memberships carry. It is not a reduced-privilege credential: an owner’s MCP token carries that owner’s full authority,add_function included. Hand it only to
an agent you would trust with the workspace itself, and treat a leaked MCP
token exactly like a leaked login.
What limits an MCP session in this release is its tool surface, not a weaker
identity. Shared MCP speaks SQL, search, catalog browsing, and table function
calls; source configuration, trace reads, and membership changes are simply
not among its tools. Configure sources and read traces in Reef; membership is
a gRPC call, as described at the end of this page.
Coral does distinguish agent credentials from user credentials, and refuses
owner-level operations to agent credentials before any role is consulted. That
refusal is enforcement-in-waiting: this release’s login flow only mints user
tokens, so no credential an MCP client can obtain today is bound by it.
A workspace you cannot reach reads as one that does not exist
Coral answers not found whenever the caller may not know a workspace exists, and says owner access is required only to a caller who already holds a membership in it. This is deliberate. Swapping the two would turn the workspace namespace into an oracle: anyone could probe for names and learn which ones are real. So a name that was never created, a workspace you are not a member of, and a workspace with no owner all produce the same answer. Treat “not found” as “not yours to see”, not as evidence about what exists on the server.Shared MCP serves each workspace at its own URL
An MCP HTTP listener with login enabled serves every workspace at its own URL,<public_url>/workspace/{workspace}:
public_url; it is the base the
per-workspace URLs hang under, not an endpoint of its own. (A deployment that
runs login without the MCP listener declares its public surface through
auth.allowed_audiences instead.) Nothing selects a
workspace in configuration: the URL a client connects to —
https://mcp.example.com/mcp/workspace/analytics for a workspace named
analytics — names the workspace its sessions reach, a workspace is
connectable as soon as it exists, and onboarding a teammate is handing them
their workspace’s URL. Each workspace URL is its own OAuth resource and token
audience, so a token minted for one workspace’s URL is invalid at every
other’s, and a session opened at one URL does not exist at another.
When a client connects, Coral lists the memberships of the calling token, using
that caller’s own credentials and nothing else, and admits the session only when
the URL’s name is exactly one of them. No workspace is substituted and none
is picked on the caller’s behalf. The decision is made once per session, at the
initialize handshake.
A refused session gets a JSON-RPC error on the initialize response rather than
a bare connection failure, so the guidance actually reaches the client:
What happens when a membership changes
No restart is needed for a membership change to take effect, and no decision is cached at startup. But when a change lands differs by surface, and the difference matters if you are revoking access in a hurry. On the gRPC API — which is what Reef and every Coral client talk to — membership is authorized per request, against the state as it stands. A change lands on that caller’s next request. On an authenticated MCP session, admission is decided once, at the handshake. That has a boundary worth stating plainly:- A caller whose membership was removed cannot open a new session. Their next
initializeis refused with the message above. - The session they already hold is not torn down. Coral matches an established session to the exact bearer token that opened it — and validates that token again on every request, expiry included. A token obtained through refresh does not resume the old session; it must initialize a new one, which is where the membership check runs again.
- The session record itself has no maximum lifetime — the one-hour session
timeout is an idle timer — but the token bounds it: when the access token
expires (
access_token_ttl_seconds, 30 days by default), the session stops being usable even if it never idled out.
Workspaces nobody can reach
A workspace with no owner is unreachable, and it is concealed from its remaining members too: a Member row in an ownerless workspace grants nothing, so that member is refused exactly as a non-member is. Coral will not put a workspace into that state on its own — removing or demoting the last owner is refused. It arises from history: workspaces created before Coral had a user directory, or state edited outside the server. A server with login enabled reports these at startup, atWARN, and then keeps
serving everything else. A workspace nobody can reach is an operator’s job to
fix, not a reason to deny every other workspace its server.
- No owner at all. Appoint one.
- Owned only by the built-in local user.
coral:localidentifies the single-user local process, not a person who can authenticate. A workspace it owns is validly owned in storage, so — unlike the ownerless case — its existing members keep their member-level access; what no signed-in user can do is manage it, because managing takes an owner who can log in. Appoint a human owner alongside it — recovery adds owners and removes none, so the local row stays and stops mattering.
Recovering a locked-out workspace
If no owner remains, no one on the server can appoint one — that is the point of the owner rule, and there is no superuser to fall back on. Recovery therefore happens outside the server, against the state database.Before you start
The person you intend to appoint must have signed in at least once. Recovery appoints an existing directory row; it never creates an identity, and it cannot invent one for somebody the server has never seen. Ask them to sign in first, then appoint them. Point the tool at a deployment the same way you point the server at it, withCORAL_CONFIG_DIR. The state directory is deliberately not a flag: recovery
reads config.toml by the server’s own rules, and for Postgres reads the
connection URL from the environment variable that configuration names, so a
mistyped path fails identically for both.
The tool must be able to reach the state database. For Postgres that is any
machine holding the connection URL, so containerized deployments are covered as
they stand. For SQLite the database file itself must be on a filesystem this
tool can touch — true for a host-run server, not for the SQLite-on-a-volume
default the container guides use. There, stop the server first (which
checkpoints the WAL), copy coral.db out of the volume, repair the copy, and
copy it back before restarting. Never copy the file out from under a running
server: a live WAL-mode database keeps recent commits in its -wal sidecar,
and a copy of coral.db alone silently drops them.
1
Find the workspace
zero-owners and local-owner-only are the two unreachable states named in
the startup warning above. One caveat: after an unclean server exit (a
crash, an OOM-kill, a reboot), a leftover -wal sidecar can refuse the
read-only open — the error says so and names the fix: start the server once
so it recovers its own WAL, then retry.2
Find the person
--show-subjects if you need them to tell two accounts apart.coral:local appears here and is marked as not appointable.3
Appoint an owner
--user takes the internal user id from the previous step, which is a UUID —
never an email address and never a provider subject.This adds or promotes, and never replaces: no other membership is removed,
demoted, or restamped, and re-running it once that person already owns the
workspace writes nothing at all. Membership is authorized per request, so the
appointment takes effect on that user’s next call, with nothing restarted.After an identity provider rename
If your issuer identifier changes, existing logins no longer match their directory rows. Coral fails those sign-ins rather than silently rebinding them to a new identity, because a silent rebind is how one person’s account quietly becomes another’s.What recovery will not do
- It never migrates. It reads and repairs an existing database exactly as the server left it, and applies no schema or state migration; bringing a database up to the current schema is the server’s job. If the state directory is behind, start the server on it first, then recover.
- It refuses
coral:localin every command, as an appointee and on either side of a rebind. Appointing the local principal would leave the workspace exactly as unreachable as it already is. - It decides nothing about who is running it. There is no authentication and no authorization in the tool.
set-owner and rebind-issuer need the database write lock. If the running
server is holding it, they fail without writing anything and say so; retry, or
stop the server for the moment the repair takes.
Managing membership today
The handoff is gRPC. Membership lives oncoral.v1.WorkspaceService, as
ListWorkspaceMembers, AddWorkspaceMember, and RemoveWorkspaceMember, and
those calls are how members are named, listed, and removed.
As of this release those calls are the only membership surface: Reef does not
manage members yet (you can create a workspace there and work in the ones you
belong to), and the CLI has no membership commands — coral workspace covers
listing, creating, and removing workspaces only. When a Reef screen for
workspace members lands, it will drive these same RPCs, and the raw calls stay
available.