Skip to main content
Coral can run as a server that more than one person signs in to. This guide is for the operator of such a server, and for the people using it who want to know why a workspace they expected to see is not there.
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 as No 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.
Nothing else grants access. No membership is derived from the identity provider either: group claims, email domains, and roles asserted upstream grant nothing in Coral. A login establishes an identity, and membership is recorded against that identity here.

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}:
Coral serves cleartext HTTP on this listener and terminates no TLS. Without an HTTPS proxy in front, every bearer token a client sends can be read off the wire, and a 0.0.0.0 bind exposes that cleartext port on every interface even when a proxy also fronts the service. Bind to an address only your TLS terminator can reach. The server prints this same warning at startup when the bind is not loopback.
With login enabled, this listener requires 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:
That is the only membership answer this surface gives, and it is the same answer in two different situations: the workspace does not exist, and the workspace exists but this caller may not reach it. Same error code, same sentence, nothing attached to tell them apart. Admission reads the caller’s own memberships and never asks whether the name exists at all, so the distinction is not withheld — it is never computed. Do not try to infer which case you are in from the response; ask an owner.
The concealment holds before login too: every well-formed workspace URL presents the identical authentication challenge and protected-resource metadata, whether or not such a workspace exists, so probing URLs enumerates nothing. Only members learn a workspace exists, by being admitted to it.

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 initialize is 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.
What still protects the data in that window is the layer underneath: every workspace-scoped operation the session performs is authorized again, per request, on the gRPC surface behind it. So a revoked caller’s open session keeps speaking the protocol while its queries, catalog reads, and searches are refused.
Do not treat removing a membership as cutting a connected agent off mid-session. It stops them starting a new one. To end the sessions themselves, restart the MCP listener — sessions are held in memory and do not survive it — and revoke the credential at your identity provider.

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, at WARN, 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.
The two categories need different repairs:
  • No owner at all. Appoint one.
  • Owned only by the built-in local user. coral:local identifies 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.
xtask workspace-admin is a repository utility, not part of a Coral release. It is compiled only with xtask’s off-by-default admin feature, and no shipped Coral binary depends on it, so it is absent from anything you install.Its entire authority is possession of the state database: filesystem access to the state directory for SQLite, or the configured connection URL for Postgres. It authenticates and authorizes nobody. Anyone who can run it against your deployment can hand themselves any workspace on it, so treat that access as equivalent to owning all of them.

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, with CORAL_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

This opens the database read-only, so it is safe against a running server. 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

The listing prints user id, display name, issuer, whether the account can be appointed, and its last-login and created timestamps. Appointable accounts come first, most recent login leading — so the person you just asked to sign in heads the list.Provider subjects are withheld by default, because the listing exists to find a user id rather than to publish everyone’s upstream identity. Add --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.
Rebinding updates the issuer and nothing else. The internal user id behind each account is a primary key this never rewrites, so no membership is orphaned and nobody who already has a directory row gets a second one.

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:local in 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.
Turning login off is not recovery. The one-time upgrade that gives the built-in local user ownership of unowned workspaces runs at most once for the life of a state directory.If it has already run, it will not run again, and a workspace that lost its owner afterwards stays ownerless. If it has not run, it runs now and hands those workspaces to coral:local — an identity nobody can authenticate as — so when you turn login back on they report as local-owner-only, which is just as unreachable to the people you were trying to help.Either way you end up running set-owner. Run it first.

Managing membership today

The handoff is gRPC. Membership lives on coral.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.