Data Hub

Security and permissions

Roles and what each can do, personal-access-token scopes, how authentication works, and good token hygiene.

ForAdministrators and developers

This page explains who can do what in Data Hub: the two roles, the scope system that gates token-authenticated requests, and how to handle tokens safely. For the mechanics of creating and revoking tokens, see Issue and revoke tokens.

Authentication

The API accepts two kinds of credentials:

  • Session cookies: used by the web dashboard, which signs people in with Google OAuth via Better Auth. Session callers implicitly hold every scope; scope checks only apply to token requests.
  • Bearer tokens: personal access tokens (dhub_…) used by the watcher, the Lambda, and scripts, sent as Authorization: Bearer <token>.
  • OAuth access tokens: issued to MCP clients after you sign in and approve a consent screen. Covered in MCP access.

Tokens are hashed with SHA-256 before storage; the plaintext is shown exactly once, at creation.

Who can sign in

Any Google account that finishes the sign-in flow becomes a member of your deployment. Data Hub itself checks nothing: there is no invite flow, no email allowlist, and no email-domain check, so a first successful sign-in creates the member record.

Restrictions live on the Google side instead, in the OAuth client your team creates at deploy time. A consent screen set to Internal accepts only accounts in your Google Workspace organization. Set to External, it accepts any Google account, including personal ones. Engineers make that choice when they create the OAuth client.

Data Hub accepts 10 Google sign-in attempts per minute from one IP address and rejects the rest.

Every new member starts in the member role, never admin. See First admin bootstrap for how the first admin gets the role.

Session lifetime

Signing in stores a session in the database and sets a signed cookie. The session expires 7 days after its last refresh, and the dashboard refreshes it at most once a day, so a session in daily use doesn’t lapse on its own.

That cookie also carries a signed copy of the session for 5 minutes, which keeps most page loads off the database. Requests read the copy instead of the row. A change made in the database directly, such as deleting a session, therefore applies when the copy expires rather than on the next request.

Revoking someone’s access on the Google side, by removing them from your Workspace organization for example, blocks their next sign-in but leaves any session already open working until it expires. Delete that user’s rows from the session table to cut them off sooner. Signing out from the sidebar ends the session immediately.

Roles

RoleCan do
MemberSign in to the dashboard; view instruments, runs, files, and the token audit list. Cannot mint or delete tokens, confirm/update instruments, or manage other users.
AdminEverything members can, plus: confirm and update instruments, create and revoke any user’s tokens, and promote/demote other users.

The first admin is bootstrapped from the ADMIN_EMAILS environment variable (comma-separated, case-insensitive); listed users are promoted on every sign-in. After that, any admin can promote others from Settings → Members. Admins cannot demote themselves: that guards against locking the workspace out of its last admin.

Admin-gated operations

A subset of mutations require the admin role in addition to (or instead of) a scope check:

  • PATCH /api/v1/instruments/:id: session callers must be admin (bearer-token automation still uses instruments:write, so watcher/Lambda flows are unaffected).
  • POST /api/v1/tokens and DELETE /api/v1/tokens/:id: admin-only, session-only. Bearer tokens cannot manage other tokens.
  • GET /api/v1/users, PATCH /api/v1/users/:userId: admin-only, session-only. Used by Settings → Members.

Token scopes

Every personal access token carries an array of permission scopes. A token-authenticated request is rejected with 403 FORBIDDEN when the token’s scopes don’t include the scope a route requires. Issue tokens with the least privilege they need. New tokens must list fine-grained scopes; POST /api/v1/tokens rejects the wildcard * and the older coarse scopes runs:write, files:write, and watchers:write. Tokens that already carry those still work until you rotate them.

ScopeGrants
instruments:readRead instruments, dashboard status, and file patterns
instruments:writeCreate instruments and edit their configuration
runs:readRead and search runs, their files, comments, and attributions
runs:createCreate run records
runs:updateUpdate run metadata
runs:deleteSoft-delete and restore runs
runs:reprocessRe-run the processing workflow for a run
runs:uploadRequest presigned S3 URLs to upload files to a run
runs:attributeClaim or unclaim runs
runs:commentAdd, edit, and delete run comments
files:readRead file metadata and download files and run archives
files:createRegister file records against a run
files:updateUpdate file metadata and upload state
files:deleteDelete files
files:reprocessRe-run the processing workflow for a file
watchers:readRead watcher status, heartbeats, and upload queues
watchers:reportRegister, heartbeat, send events, and push watcher config
watchers:adminDelete watchers
archive-jobs:readRead run-archive job status
archive-jobs:writeUpdate run-archive job status (Lambda callback)
*Matches every scope. Reserved for legacy backfilled tokens; rejected on new tokens

A watcher token typically needs instruments:read, instruments:write, watchers:read, watchers:report, runs:create, runs:update, runs:upload, and files:update. That is what the Watcher preset selects. A read-only analysis token needs the :read scopes for the data you want to query.

MCP access

The MCP server at /mcp/v1 doesn’t accept personal access tokens. An AI client runs an OAuth flow instead: you sign in to Data Hub, approve a consent screen that names the client and the access it wants, and the client receives an access token valid for that deployment’s MCP endpoint alone. A token issued to a client with no person behind it is rejected, so every MCP action attributes to a user.

MCP recognizes two scopes rather than the <resource>:<action> pairs above:

  • read: required to connect, and covers every read-only tool, resource, and prompt
  • write: additionally required by any tool that changes data, such as claiming a run or deleting one

A grant belongs to one client and one person, and it carries that person’s own permissions. Clients register themselves at connect time, which creates an OAuth client record but grants nothing on its own: Google sign-in still decides who gets through, and the consent screen still decides what they hand over.

The dashboard has no screen for reviewing or revoking MCP grants yet. Removing someone from your Google Workspace organization blocks new grants; cutting off a client that already holds a token means deleting the matching rows from the oauthAccessToken and oauthRefreshToken tables.

Client setup is in MCP overview.

Token hygiene

  • Name tokens descriptively (e.g. “FPLC watcher, Lab 201”) so you can tell what each one is for.
  • Set expirations for temporary setups.
  • Scope to least privilege: don’t hand out broad tokens for a single read-only integration.
  • Revoke immediately when a watcher is decommissioned or a token may have been exposed. Revocation is instant; clients using the token start getting 401 Unauthorized. See After revoking a token.

Public pages, gated bodies

Some web pages (dashboard, instruments, run detail, settings) are reachable without a session so link unfurlers can read page metadata. The page body renders a sign-in prompt instead of real data when there’s no session, and three independent layers (robots metadata, robots.txt, and an X-Robots-Tag header) keep the product app out of search indexes. The /api/v1/* surface always requires a session cookie or bearer token.

On this page