Identity and access
Understand accounts, roles, grants, API keys and deployment modes.
BytesMap uses OpenShip's contracts → platform → host/SDK structure for accounts, workspaces, grants, and API keys. The dashboard, HTTP API, and native SDK call the same authorization and access services. The packaged desktop and its Mindwire assistant use this boundary too; local tooling does not create a second authority model.
Ownership
| Layer | Owns |
|---|---|
packages/contracts | Roles, permissions, operation inputs/outputs, HTTP paths, and MCP opt-in metadata |
packages/platform | Verified execution context, authorization, shared operation lifecycle and Better Auth factory |
packages/platform/src/engine/modules | Sources, servers, queries/views, flows, reports, teams, keys, delivery and audit services, following OpenShip's folders |
packages/db | Metadata schema, additive upgrades, and transaction-aware repositories for Postgres/PGlite |
packages/sdk | Typed HTTP client and native adapter over the same product/access operations |
apps/cli, packages/bytesmap | Catalogue-driven commands and SDK/API/worker distribution entrypoints |
apps/api | Request authentication, Origin/peer checks, session transport, routing, and MCP transport |
packages/ui | Shared access editor, controls, tokens, and themes |
apps/dashboard | Settings and sign-in forms; it never decides the caller's authority |
apps/desktop | Electron shell, private local services, protected host keys, navigation and process lifetime |
Query/flow/chart protocols and pure execution remain in their shared packages. Product services run through the platform's operation boundary across HTTP, MCP, CLI and native SDK. The catalogue covers sources, servers, queries, charts, dashboards, flows, reports, reporting, notifications, permissions, tokens and audit. See architecture and SDK/CLI usage.
Workspace owners/admins manage notification channels and scheduler/SMTP settings with an account session. API keys and MCP cannot configure outbound destinations. Report authors choose safe channel summaries without receiving credentials. See scheduled reports for delivery and verification behavior.
A request's identity
Explicit API credentials are validated first. A real signed-in session then takes
precedence over the anonymous desktop account, including in auth=none mode.
Only a request without a session can use the guarded local fallback. Session
lookup failures never select a different account. Workspace selection stays
bound to that account's current membership.
- The host verifies a Better Auth session, an API key, or an explicitly enabled local device connection.
- It resolves an organization from a key binding, an explicit requested scope, the signed workspace cookie, or the user's membership list.
- The platform loads the live user, membership, role, and credential. It produces
a frozen
ExecutionContext; role and identity fields never come from an operation body. - The operation validates its input, checks access, performs the work, and validates its public response. Mutating access operations recheck authority inside the transaction that changes it.
Contexts include the user, membership id, workspace, session kind, credential id, scope, and request source. Membership changes, expiry, revocation, and removal apply to subsequent operations, including already-resolved native contexts. Background work also carries the membership id so leaving and rejoining a team cannot revive an older job's authority.
HTTP is fixed to the selected workspace. A native host can deliberately request resource-based scope, which resolves a resource's owning workspace and checks membership there again. A role from one workspace is never reused in another. Keys always remain fixed to their bound workspace.
Roles and grants
| Role | Workspace access |
|---|---|
| Owner | Product operations and team administration; can change owners |
| Admin | Product operations and team administration; cannot change owners |
| Member | Sources, queries, charts, dashboards, and execution; no team/settings/billing administration |
| Viewer | Read/list; no query execution |
Custom access (restricted) | Granted projects/items and their allowed actions |
Grants use { resourceType, resourceId, permissions }. * deliberately grants
the whole resource collection. read, write, admin, list, and run are
semantic actions, independent of the HTTP method. write includes reading;
admin includes management but does not imply run for a custom grant.
Schema, profile, model, and SQL permissions are rooted in their source. Queries, charts, dashboards, and servers have their own grants. Executing a saved query also checks the sources it touches. Lists filter inaccessible rows; dashboard responses filter nested charts, queries, and sources too. Reusing a saved server's SSH connection while adding a source requires access to manage that server.
The same grant validator is used for keys, custom members, and invitations. It checks every action, actual resource ownership, and wildcard authority. The UI loads only resources and actions the current caller can grant.
Projects have stored membership and inherit their grants to their queries, charts, actions, panels, dashboards and folders. Individual item shares remain additive and do not reveal neighboring items. Project Write permits authoring its items; Run and source/data permissions remain separate. Moves and project removal change inherited access immediately without copying grants to items. Keys and agent tools apply the same intersection with current member access. The sidebar project choice controls navigation, never authority or ownership. Canonical project links select that context; old unscoped resource links resolve stored ownership independently of the remembered collection filter.
Workspaces and invitations
New accounts receive a workspace with one owner. Settings → Workspace → Add
people invites directly to that workspace. Its first valid invitation marks it
as shared in the same transaction; its ID, sources, queries, charts, dashboards,
memberships, and API keys stay in place. A rejected invitation request leaves the
workspace unchanged. The SDK's createTeamOrg operation remains available when a
caller explicitly wants a separate workspace; it is not a prerequisite for adding people.
Workspace switching persists the verified account session's active organization.
The shared resolver validates that membership on every operation, keeps a valid
selection, and otherwise prefers the first team before the personal workspace.
Sessionless local access uses a signed, account-bound navigation preference.
Selecting a workspace never proves membership.
Workspace owners/admins can invite members by a copyable link or configured SMTP, change roles, edit custom grants, and remove members. The last owner cannot be removed or demoted. Removing or leaving a workspace revokes its keys and grants, clears stale session selections, and removes notification subscriptions in the same transaction. A self-leave cannot remove the last owner.
Invitations expire after seven days. Acceptance checks the recipient's signed-in
email, current status/expiry, and the sender's current authority, then creates
membership and grants atomically. Acceptance can be retried safely. Cancellation
removes pending grants. If email delivery fails after creation, the response still
returns the saved invitation link with deliveryStatus: 'failed'.
The Better Auth organization plugin remains installed for schema/session support. Its alternate organization mutation endpoints are blocked. Team lifecycle goes through the shared platform operations in every host.
API keys and MCP
Create a key in Settings → API keys. Keys are bound to the current workspace, displayed once, and stored only as a SHA-256 hash plus a short display prefix. The list includes expiry, revocation, last use, and request count.
Key authority is the intersection of the owner's current membership/grants and the key's selected grants. An empty selection cannot silently become full access; full access requires an explicit choice. Read-only keys permit read/list only, including for POST endpoints whose action is read. They cannot run queries.
Keys cannot create more credentials or manage accounts/team authority, even when full access was selected. Those operations require an account/device session.
MCP is a stateless Streamable HTTP endpoint at /api/mcp. Give a key the MCP
connection permission plus its desired resource permissions. Tools are generated
from the registered routes/contracts, filtered against current capabilities, and
dispatched through the real app. A process-local WeakMap<Request, Context> passes
verified identity to the internal request; headers cannot fabricate that context,
and one tool request increments key usage only once.
MCP includes query/chart/dashboard authoring and updates. A key can author without execution permission. Separate run/preview tools retain the same source checks as the UI. Account, key, membership, and invitation mutations are not exposed as agent tools. See MCP setup.
On hosted endpoints, OAuth MCP clients use the same live grant-holder model as
keys. Consent chooses one workspace and uses the shared resource editor; connected clients can be
edited or disconnected from Settings. Tokens are audience-bound to /api/mcp,
require S256 PKCE, and cannot create credentials or change account/team authority.
Reconnect creates a new binding so a revoked token never becomes valid again.
The packaged desktop has a different external transport: its private host accepts MCP POST requests from direct loopback clients only with an expiring, explicitly scoped API key and the MCP connection permission. Browser cookies, OAuth tokens, Origin/proxy headers and broad keys do not enter that path. Ordinary REST and account routes still require the owning desktop's private capability. Settings reads the host's declared endpoint and authentication capabilities instead of promising OAuth on every host. The CLI's desktop stdio transport discovers the current per-launch identity, verifies a fresh proof, and sends the scoped key on the same non-reconnecting TCP connection. It never uses the private host capability.
Desktop assistant conversations have private, non-exportable grant holders too. The caller must have an account/device session on a locally verified desktop host. Each conversation belongs to one user and workspace. Permission changes stop its tools and require fresh model context; approving a tool never grants additional access. These services are unavailable to API keys, OAuth clients, cloud, and self-hosted hosts. See native assistant.
Deployment and authentication modes
Edition and authentication mode are independent:
| Edition | Host configuration | Current account behavior |
|---|---|---|
| Desktop | DEPLOY_MODE=desktop | Packaged Electron host starts private loopback services and explicitly provisions a pinned local identity |
| Self-hosted/VPS | DEPLOY_MODE=bare or docker | Instance-local accounts; first-admin setup followed by invitation-only signup |
| Cloud | DEPLOY_MODE=cloud or CLOUD_MODE=true | Accounts and workspaces hosted by the cloud API; verified email required |
BYTESMAP_AUTH_MODE explicitly pins none, local, or cloud. When unset, the
resolver reads instance_settings; a missing value or read failure defaults to
local, never anonymous access. The cloud auth mode has a shared desktop login
handoff implementation with stored one-use attempts, PKCE and local session
claims. Cloud edition hosts its own Better Auth accounts. The packaged desktop
starts in guarded none mode; connecting that local workspace to a cloud account
is a separate, explicit operation.
none additionally requires an actual kernel-reported loopback peer, no public
URL, no require-auth flag, and either desktop mode or
BYTESMAP_ALLOW_ZERO_AUTH=true. Host/forwarded headers do not satisfy it. Every
dashboard/proxy in a local deployment must also bind to loopback; a public proxy
must use authenticated mode. An invalid supplied key never falls back to local
identity. Deleting the pinned local user cannot select another account silently.
The packaged desktop launcher starts the existing Next dashboard and Bun API on separate IPv4 loopback ports. It isolates its PGlite store from development data, loads persistent keys through the operating system credential store, and supplies a fresh private capability for each launch. Its owning IPC channel is required; losing that owner stops the API. Only this explicit desktop startup, or the native CLI's local setup, provisions a pinned device identity. Ordinary network API startup does not provision an anonymous administrator. See the desktop host guide.
First account and upgrades
For a fresh self-hosted/VPS instance:
- Choose persistent PostgreSQL or embedded PGlite metadata. API startup applies the shared baseline/additive migrations automatically.
- Set a generated
BETTER_AUTH_SECRET,BYTESMAP_AUTH_MODE=local, andBYTESMAP_REQUIRE_AUTH=true. SetBYTESMAP_PUBLIC_URLfor public hosting. - Generate a host-only setup token, for example
openssl rand -base64 32, and setBYTESMAP_SETUP_TOKEN(at least 24 characters). - Open
/setup, enter that token, and create the first administrator.
Local desktop/CLI setup may use the same independently checked loopback proof. The setup token ceases to authorize signup once an account exists. Subsequent self-hosted accounts require a valid invitation for the submitted email.
First-account checks use a metadata row lock across concurrent requests. User, credential, session, and personal workspace creation commit together; a failed provision cannot leave a half-created administrator or return a session cookie. Signup and other account routes retain Origin checks and their existing rate limits.
Startup applies the shared metadata migrations before constructing the auth adapter. Upgrades include passkeys, two-factor fields, invitation grants, key usage, report settings and audit tables. They do not rewrite roles or reconnect customer databases. Legacy keys without a workspace binding are refused and must be recreated.
Account security
The shared auth factory pins Better Auth, its core, and passkeys to 1.6.26. OpenShip's passkey/TOTP guards were adapted to that version's challenge handling. Security endpoints join one transaction, serialize credential changes on the user row, reject stale cached sessions, and preserve failed-code attempt budgets.
Settings supports profile/password changes, session revocation, passkeys, authenticator setup, and recovery codes. A working second factor cannot be replaced by a concurrent enrollment. Enabled two-factor authentication also applies to non-password sign-in hooks; no primary sign-in can issue its final session before the challenge. Invitation destinations survive verification and two-factor navigation.
Email verification/recovery and invitation mail use the host's configured email transport. The platform owns reusable delivery logic; hosts inject configuration or alternative adapters. It never prints credentials.
Activity and request logs
Settings → Activity and client.audit expose workspace-scoped operation history:
actor, action, resource, outcome, duration and server-issued trace ID. Owner/admin
accounts can change recording and retention. Custom members and keys may receive
audit read access; it is not enabled for ordinary members/viewers by default.
The shared observer records mutations and execution, including native calls. Account creation and session lifecycle have shared hooks. Routine successful reads are omitted. Audit payloads exclude SQL, request bodies, result data and credentials. Logging is best effort and does not roll back a committed operation. It is not an exhaustive record of every failed sign-in or network denial.
HTTP diagnostics use structured logs and x-request-id, without query strings,
headers or raw error payloads. MCP's internal dispatch preserves that trace and
does not duplicate the key-use count or operation event. Report delivery retains
its separate schedule/run history. The shared scheduler prunes expired activity.
Desktop integrations and release limits
Mindwire is integrated through its TypeScript SDK and native daemon. The desktop assistant discovers installed Claude Code/Codex harnesses, manages private profiles, and gives each conversation a permission-scoped BytesMap tool bridge. Model arguments cannot choose an identity or filesystem path; native execution tools are restricted by the host's harness policy. The current native assistant requires macOS or Linux on ARM64/x64; the Windows desktop shell does not imply Windows assistant support. Cloud and self-hosted hosts do not launch it. See native assistant for supported capabilities and fixture coverage.
Cloud workspace linking has a packaged external-browser return path. The desktop accepts only a registered, unexpired state from its configured cloud authorization origin and consumes it once. The platform independently checks the stored PKCE verifier, authorization code, local owner and workspace. The packaged smoke verifies browser-to-desktop navigation using a local redirect fixture; it intentionally does not mint a real cloud session. Separate platform tests cover the exchange and identity rules. Real provider login is still a release gate.
macOS arm64 packaging and isolated native tests have local evidence. Signed releases, signed Keychain/DPAPI continuity across versions, and the remaining platform installation checks need their own results. Automatic updates are not shipped. Desktop discovery and the scoped CLI stdio bridge have local Node/Bun integration evidence; Windows account-directory ACL and native installation checks still need their platform results. The launch-readiness record tracks these gates; fixture login and an unsigned package are not evidence of a production provider or signed release.
Verification
packages/platform/test uses isolated PGlite databases for access, invitations,
key revocation, first-account rollback, real WebAuthn/TOTP, and concurrent security
changes. apps/api/src/lib/identity-http.test.ts starts a separate API fixture and
checks HTTP/native SDK/CLI parity, scoped MCP authoring, shared audit, setup races, origin/peer
guards, workspace isolation, and revoked authority. It never connects to customer
data sources. The same fixture runs against disposable PostgreSQL metadata in the
real database integration job; native hosts also have isolated PGlite tests.
packages/platform/test/mcp-oauth.test.ts verifies consent, S256 PKCE, audience
binding, refresh rotation and live revocation. cloud-auth-proxy.test.ts and
cloud-link.test.ts cover local/cloud identity and one-use exchange using isolated
fixtures. Desktop host integration checks private transport and process cleanup;
its packaged smoke checks navigation, rendering and the external-browser return.
Native assistant tests use the real Mindwire daemon with fixture CLIs and local
scripted model responses, without paid inference or real provider sign-in.
apps/dashboard/e2e/auth.e2e.ts and settings.e2e.ts cover user flows and
Light/Dark/Dim layouts with keyboard and narrow-viewport checks. Their HTTP fixtures
exercise the real browser SDK; backend behavior is checked separately by the API
and platform integration tests.