Access Control
Zelavis uses one core access model for the control plane, project dashboards, service endpoints, CLI calls, AI agents, scripts, and future official modules such as Hosting Provider.
The dashboard is not the authority layer. Menus and project cards are filtered for usability, but endpoint access is enforced by the server contract.
Core Shape
Section titled “Core Shape”The base model lives in zelavis/core:
- a
ZelavisPrincipalrepresents the caller - roles describe broad identity shape such as owner, operator, reseller, or customer
- permissions describe concrete capabilities
- grants attach permissions to a scope
- access requirements are declared on routes and service-owned menu items
Scopes can be:
systemfor owner/operator actions that affect the whole Zelavis instanceprojectfor project, site, app, and customer-owned resourcesservicefor service/plugin-owned capabilities
Example route requirement:
{ id: "projects.content.update", method: "PATCH", path: "/:projectId/content/:entryId", access: { permissions: ["project.content.write"], scope: { type: "project", projectIdParam: "projectId" }, }, handler({ principal, params }) { return { body: { principalId: principal?.id, projectId: params.projectId, }, }; },}Why This Is Not Only zelavis/app/identity
Section titled “Why This Is Not Only zelavis/app/identity”zelavis/app/identity owns authentication building blocks: accounts, credentials,
sessions, and pluggable auth methods such as email/password, username/password,
passkeys, OAuth, SSO, API keys, or service-token providers.
zelavis/core owns the lower-level authorization contract because every
runtime service needs a way to declare and enforce access requirements without
depending on one authentication method.
That separation keeps the core extensible:
- a dashboard session can resolve a principal
- an API key can resolve a principal
- an AI agent token can resolve a principal
- a Hosting Provider customer login can resolve a principal
- a future SSO plugin can resolve a principal
All of those callers still pass through the same route access checks.
Native Request Authentication
Section titled “Native Request Authentication”The runtime composes ZelavisRequestAuthenticator implementations directly on
the standard Web Request boundary. No router framework owns authentication.
- opaque App sessions use 256-bit random
zvs_tokens and persist only a SHA-256 token digest - App accounts, credentials, and sessions persist through the App’s tenant-routed database boundary
- Platform accounts, credentials, and sessions persist separately in the System Store
- session credentials may arrive through
Authorization: Beareror an HttpOnly, SameSite cookie that is Secure on HTTPS deployments - scripts receive the issued bearer token directly; a response sets the
session cookie only for a request carrying a matching same-origin
Origin - cookie-authenticated mutations require a matching same-origin
Origin - first-owner bootstrap requires an operator-configured high-entropy token and a credential-enrollment provider; the token is never a normal login method
- optional Basic, JWT, and remote-JWKS authenticators resolve into the same principal model
- invalid credentials produce a
401and the appropriateWWW-Authenticatechallenge
Password sign-in is part of Zelavis rather than a plugin. An installation with
no credential provider can never create its first owner, so it is not something
an extension supplies. One provider covers both identifier kinds: an identifier
containing @ is treated as an email address, anything else as a username. The
password primitive uses salted PBKDF2-HMAC-SHA-256 over Web Crypto, and an
unknown identifier is verified against a decoy hash so the response does not
reveal whether an account exists.
The OAuth Authorization Code flow is core too — it holds the state, nonce and
PKCE verifier, which are the parts that are dangerous to get wrong and the same
for every provider. Any OpenID Connect issuer is added by pasting its issuer
URL; a plugin declaring zelavis/identity:oauth is needed only for a provider that
is not OIDC, where the profile endpoint and claim mapping are real code. The
OIDC plugin under plugins/ is now only a bearer-token authenticator for
callers already holding a token from an issuer
on top of that verifier.
Dashboard Views
Section titled “Dashboard Views”The same /zelavis shell can render different views depending on the current
principal:
- owner or superadmin: all projects plus server, domains, resources, security, marketplace, and project controls
- operator: delegated server/project controls
- reseller: projects and customers owned by that reseller
- customer: only their projects and allowed project actions
Project creation can assign an initial Project-owner grant, but the creator ID is not an authorization shortcut. Owners and superadmins can see all Projects regardless of who created them; every other inventory item and action is filtered by explicit grants and still enforced by its endpoint.
This is the intended foundation for a future official Hosting Provider module. The module should not create a separate customer permission system. It should create customers, subscriptions, packages, and grants that feed the same core principal model.
Access itself is a native Platform capability, implemented with the reusable
access contracts from zelavis/core. Its dashboard navigation is contributed by
@zelavis/ui through the normal service menu API on the platform surface,
which is reserved for bundled or statically trusted system services that belong in the
global /zelavis management shell. Runtime-installed marketplace plugins still
mount under Extensions.
Platform Bootstrap And Login
Section titled “Platform Bootstrap And Login”GET /zelavis/api/v1/auth/bootstrap reports whether the installation still
needs its first owner and which installed auth providers support credential
enrollment. POST /zelavis/api/v1/auth/bootstrap is disabled unless the
operator configured ZELAVIS_BOOTSTRAP_TOKEN or bootstrap.token; successful
bootstrap consumes an installed provider’s enrollment contract and issues the
first owner session.
The dashboard login screen calls the same versioned Auth endpoints as other
clients. /zelavis/api/v1/runtime/access requires authentication and returns
the real session principal. There is no demo-owner fallback or query-string
identity switch.
App Data Is Its Own Authority
Section titled “App Data Is Its Own Authority”A Project’s records are not part of its control plane, and the permissions say so:
project.viewcarriesdatabase.inspectanddatabase.read— an operator browsing an installation’s tables, including other Tenants’.project.runtime.managecarriesdatabase.write, alongside the workload and storage authority that managing a runtime implies.project.data.readandproject.data.writecarrydatabase.readanddatabase.writeand nothing else.
The last pair exists so an App can hold its own data without holding authority over the Project it lives in. A Gateway request for App data forwards only those two implications, even when the caller happens to hold Project authority as well, so an App data path cannot spend a permission it did not come to use.
The Tenant travels the same way. The Platform resolves it from the
authenticated principal and signs it into the Gateway envelope; a Project
service reads it from the principal rather than from the request. A caller may
still name a Tenant, but naming one that is not its own requires
database.inspect and is otherwise refused. See App Data.
Rule For Services
Section titled “Rule For Services”Any service or plugin that performs a privileged action should:
- Define a capability in the service/runtime layer.
- Expose it through a stable endpoint.
- Declare route access requirements.
- Optionally declare menu access requirements so the dashboard can hide unavailable UI.
- Never rely on hidden dashboard state as the only protection.