Gonvex

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.

!
Project auth is enforced by the runtime

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:

  • personal creates a personal workspace and owner membership on first sign-in;
  • inviteOnly accepts 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

On this page