Auth and Membership
Users, tenant membership, roles, and permissions in Gonvex applications.
Auth and Membership
Gonvex needs a runtime-level auth and membership model because users can belong to multiple tenants, and tenants can route to different databases.
Native Google login
Google login works for both single-database and multi-tenant projects without a Firebase CLI or Google Cloud CLI in the application workflow. For a new app, the complete provision-and-wire path is one command:
npm create gonvex@latest my-app -- \
--runtime-url https://gonvex.example.com \
--google-auth \
--origin https://my-app.example.com
For an existing project:
npx gonvex auth add google --origin http://localhost:5173
The command performs three project-scoped operations:
- registers the exact app callback URL with the Gonvex runtime;
- records the provider in
gonvex.json; and - writes
gonvex/auth.tsx, a configured React provider, hook, and Google button.
Wrap the application once:
import { GonvexAuthProvider } from "../gonvex/auth";
root.render(
<GonvexAuthProvider client={gonvex}>
<App />
</GonvexAuthProvider>,
);
Then render the button and read the verified account:
import { GoogleSignInButton, useGonvexAuth } from "../gonvex/auth";
function Account() {
const { user, error } = useGonvexAuth();
return (
<>
<GoogleSignInButton className="google-button" />
{user ? <p>Signed in as {user.email}</p> : null}
{error ? <p role="alert">{error}</p> : null}
</>
);
}
The default callback is the app root (/). Frameworks must serve the application
entry point for the configured callback path. Register local, preview, and
production origins together by repeating --origin:
npx gonvex auth add google \
--origin http://localhost:5173 \
--origin https://preview.example.com \
--origin https://app.example.com
# Retire a preview origin without disabling Google for the app.
npx gonvex auth remove google --origin https://preview.example.com
List the accounts created for a project with:
npx gonvex auth status
npx gonvex auth doctor
npx gonvex auth users
auth doctor exits non-zero until the runtime broker, provider, and exact local
callbacks are production-ready. gonvex create --google-auth runs this check
automatically when a production HTTPS origin is supplied.
Backend functions receive the account as ctx.User. The user ID is generated per
Gonvex project, so the same Google account is not exposed as one shared identifier
to unrelated applications.
Registered HTTP functions use the same runtime boundary: once Google is enabled,
anonymous requests receive 401, and a valid project session populates ctx.User
and ctx.Permissions. For a direct fetch, obtain the token from
useGonvexAuth().fetchAccessToken(...) and send it as
Authorization: Bearer <token>.
Why no app needs a Google Cloud project
Gonvex acts as the OAuth broker, following the same basic design as Shoo. A hosted Gonvex installation registers one Google web OAuth client and one callback:
app -> Gonvex /auth/google/authorize -> Google
Google -> Gonvex /auth/google/callback
Gonvex -> app /?code=...
app -> Gonvex /auth/token (one-time code + PKCE verifier)
Gonvex -> project-scoped session + account
The app callback never has to be registered with Google. It is registered with Gonvex and matched exactly before the flow starts. Authorization codes are short-lived, single-use, and bound to the app callback with PKCE S256. Gonvex also verifies Google's ID-token signature, issuer, audience, expiry, and nonce before it creates an account. Stored session, refresh-token, and code values are hashed. Browser access tokens last 15 minutes. Refresh tokens last up to 30 days, rotate on every use, and revoke their full login family when stale-token reuse is detected. Signing out can revoke one login or every device.
The operator still performs one Google Cloud setup for the Gonvex installation; see Self-hosting and Deployment. Apps created on that installation do not repeat it.
Enabling Google makes that project require a valid Gonvex app session even when the runtime also hosts public development projects. Hiding data behind a sign-in component is not the security boundary.
Multi-tenant signup and membership
Single-database and multi-tenant projects support two signup modes. In a
single-database project, personal is open signup into the shared project scope;
inviteOnly requires membership in that scope. In a multi-tenant project:
personalcreates a personal workspace and owner membership on first sign-in;inviteOnlyaccepts only verified Google emails with a pre-created invitation.
Choose the mode when enabling auth:
npx gonvex auth add google --origin https://app.example.com --signup-mode inviteOnly
npx gonvex auth tenants create "Acme" --owner owner@acme.example
npx gonvex auth tenants list
For an invite-only single-database project, use the Gonvex project ID as the tenant scope when adding its first owner:
npx gonvex auth memberships add --tenant <project-id> --email owner@example.com --role owner
Manage application memberships and account lifecycle from the CLI:
npx gonvex auth memberships add --tenant <tenant-id> --email teammate@example.com --role member
npx gonvex auth memberships list --tenant <tenant-id>
npx gonvex auth memberships remove --tenant <tenant-id> --user <user-id>
npx gonvex auth memberships remove --tenant <tenant-id> --email pending@example.com
npx gonvex auth user disable <user-id>
npx gonvex auth user enable <user-id>
npx gonvex auth user delete <user-id>
The React hook exposes the same app-facing lifecycle:
const auth = useGonvexAuth();
await auth.setActiveTenant(tenantId);
await auth.createTenant("Another workspace");
await auth.inviteMember(tenantId, "teammate@example.com", {
role: "viewer",
permissions: { "tasks:read": true },
});
await auth.revokeInvitation(tenantId, "teammate@example.com");
await auth.signOut({ allDevices: true });
Invitations expire after seven days and can be revoked before they are claimed. Tenant selection is verified against the central membership registry before the runtime opens a tenant database. Role or permission changes revoke the affected user's existing sessions, and the last owner cannot be removed, disabled, or demoted.
Core Model
User
A person or service identity. Users are global to the runtime/project, not stored only inside one tenant DB.
Tenant
A customer/account/workspace under a project.
Membership
A relationship between a user and a tenant.
Role
A named bundle of permissions within a tenant.
Permission
An action the user can perform, such as tasks:read or tasks:write.
Why Users Are Separate From Tenants
One user can belong to multiple tenants:
user: gabriel@example.com
tenant: acme role: owner
tenant: globex role: admin
tenant: demo role: viewer
If every tenant has a separate database, the runtime still needs a central place to know that the same user can access all three tenants. That belongs in the landlord database.
Request Auth Flow
browser sends auth token
-> runtime verifies token
-> runtime resolves user
-> browser selects tenant or runtime uses default tenant
-> runtime checks membership
-> runtime loads role/permissions
-> function executes with scoped context
CLI Account Authentication
The CLI supports two account authentication modes:
- an expiring dashboard account session obtained with email/password
- a revocable account personal access token beginning with
gvx_pat_
Personal access tokens are stored hashed in the landlord database, belong to one
account email, can expire, and carry an explicit permission list. projects:* grants
broad project lifecycle access while still applying the owner's project membership.
Dashboard administrators can add admin:projects to make a token runtime-wide; the
profile's Admin API keys panel creates that combination for CI and provisioning.
Non-admin accounts cannot grant admin:projects or *. The provisioning-oriented
default is projects:read, projects:create, and projects:keys:read.
Account credentials authorize control-plane operations such as listing and creating
projects. Project keys remain separate, project-bound credentials used for code sync
and project environment operations. The CLI keeps account credentials in its global
user config and writes only the returned project ID/key into an app's .env.local.
Tenant Switching
The native auth hook switches to one of the signed-in user's verified memberships:
await auth.setActiveTenant("acme");
After switching tenants:
- active subscriptions resubscribe under the new tenant context
- cached rows from the previous tenant are cleared from the current scope
- generated bindings stay the same because functions are project-level
Function Authorization
Functions receive the verified user and permission payload in their runtime context. Apply the project's permission policy before accessing data:
func CreateTask(ctx *gonvex.MutationCtx, args CreateTaskArgs) (Task, error) {
if ctx.User == nil {
return Task{}, errors.New("authentication required")
}
if !can(ctx.Permissions, "tasks:write") {
return Task{}, errors.New("permission denied")
}
return insertTask(ctx.Context, ctx.Tx, ctx.TenantID, args)
}
can is application policy code; Gonvex exposes ctx.Permissions as the
membership-derived JSON object rather than prescribing one permission schema.
Control-plane operations
The runtime and CLI expose:
- users list
- tenant membership list
- invite user to tenant
- remove user from tenant
- role and permission assignment
- active tenant switcher
- project-level admins
- tenant-level owners and admins
Data Placement
The landlord database stores:
users
projects
tenants
memberships
roles
permissions
tenant database routes
auth provider links
Tenant databases store application data:
tasks
files
comments
domain tables
This separation lets the runtime authenticate and route before opening tenant-specific data connections.
Auth Providers
Gonvex supports native brokered Google login and retains the lower-level token path for external identity providers. Longer-term provider targets include:
- Clerk
- Auth0
- WorkOS
- custom JWT
- local dev auth
Provider identity is mapped to a project-scoped Gonvex user in the runtime control database. An application can use native Google login without installing any Google or Firebase package.
Security Rule
Never trust tenant/project IDs only because the browser sent them.
The runtime must verify:
authenticated user
belongs to active tenant
has permission for requested operation
is routed to the correct tenant database