Supabase auth
Supabase auth for self-hosting
Use Supabase Auth when your PandaStack deployment has more than one human user, more than one organization, or a dashboard exposed beyond a single trusted development machine.
Local development can run in stub auth mode. Production-like self-hosting should use Supabase or another OIDC/JWT-compatible identity provider.
When to use it
Choose Supabase auth when you need:
- dashboard sign-in for multiple users
- organization membership and invitations
- API requests tied to a real user identity
- JWT verification instead of the local
pds_local_dev_token - a path to hosted auth without operating an identity service yourself
Do not use stub auth for shared hosts or internet-facing deployments.
Required environment variables
API
PANDASTACK_AUTH_MODE=supabase
SUPABASE_URL=https://YOUR_PROJECT.supabase.co
SUPABASE_ANON_KEY=YOUR_PUBLIC_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY=YOUR_PRIVATE_SERVICE_ROLE_KEY
SUPABASE_JWT_SECRET=YOUR_JWT_SECRET
PANDASTACK_DATABASE_URL=postgres://...
PANDASTACK_CLICKHOUSE_URL=http://...SUPABASE_URLis used to discover the project and validate issuer/audience assumptions.SUPABASE_ANON_KEYis safe for browser-adjacent configuration but should still be managed as config.SUPABASE_SERVICE_ROLE_KEYis server-only. Never expose it to the dashboard bundle.SUPABASE_JWT_SECRETis used by the API to verify Supabase-issued access tokens.
Dashboard
NEXT_PUBLIC_PANDASTACK_API=https://api.pandastack.ai
NEXT_PUBLIC_PANDASTACK_AUTH_MODE=supabase
NEXT_PUBLIC_SUPABASE_URL=https://YOUR_PROJECT.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_PUBLIC_ANON_KEYOnly NEXT_PUBLIC_* values are shipped to the browser. Do not add service-role keys or JWT secrets to dashboard environment variables.
Supabase project setup
- Create a Supabase project.
- Open Project Settings → API.
- Copy the project URL into
SUPABASE_URLandNEXT_PUBLIC_SUPABASE_URL. - Copy the anon public key into
SUPABASE_ANON_KEYandNEXT_PUBLIC_SUPABASE_ANON_KEY. - Copy the service-role key into
SUPABASE_SERVICE_ROLE_KEYfor the API only. - Open Project Settings → API → JWT Settings and copy the JWT secret into
SUPABASE_JWT_SECRET. - Configure redirect URLs before inviting users.
Redirect URLs
In Supabase Auth URL configuration, add:
http://localhost:3000/auth/callbackfor local dashboard testinghttps://YOUR_DASHBOARD_HOST/auth/callbackfor your deployed dashboardhttps://YOUR_DASHBOARD_HOST/loginas an allowed site URL or redirect target if your deployment uses a login page
Keep localhost redirects only for development projects or explicitly trusted deployments.
Database migrations
On first boot, the API and agent apply idempotent migrations against the configured Postgres database. For Supabase auth, the important tables are:
orgsfromagent/migrations/postgres/00010_orgs_billing.sqlorg_membersfromagent/migrations/postgres/00010_orgs_billing.sqlorg_invitesfromagent/migrations/postgres/00010_orgs_billing.sqluser_current_orgfromagent/migrations/postgres/00010_orgs_billing.sqlapi_tokensfrom the API token store bootstrapsandboxes,snapshots,allocations,network_state,boot_events,usage_events,agents,leases, andsandbox_eventsfrom the agent Postgres migrationsprocessed_stripe_eventsandpending_meter_eventswhen billing/metering paths are enabled, even if Stripe is disabled in self-host mode
ClickHouse is initialized separately for observability tables such as sandbox_metrics, sandbox_events, audit_log, boot_events, and http_requests.
The Supabase Auth schema remains owned by Supabase. PandaStack stores its own organization and workspace records in the application database.
First-user provisioning
After the first user signs up, seed an organization and mark that user as owner. Replace the values with the Supabase user ID and email from the Auth dashboard:
INSERT INTO orgs (slug, name, owner_user_id)
VALUES ('default', 'Default', 'SUPABASE_USER_ID')
ON CONFLICT (slug) DO UPDATE SET owner_user_id = EXCLUDED.owner_user_id
RETURNING id;
INSERT INTO org_members (org_id, user_id, email, role)
SELECT id, 'SUPABASE_USER_ID', '[email protected]', 'owner'
FROM orgs
WHERE slug = 'default'
ON CONFLICT (org_id, user_id) DO UPDATE SET role = 'owner', email = EXCLUDED.email;
INSERT INTO user_current_org (user_id, org_id)
SELECT 'SUPABASE_USER_ID', id
FROM orgs
WHERE slug = 'default'
ON CONFLICT (user_id) DO UPDATE SET org_id = EXCLUDED.org_id, updated_at = now();Then refresh the dashboard session. The first user should see the seeded organization.
Troubleshooting
401 invalid token
Check that the dashboard is sending the Supabase access token, not the anon key. The anon key identifies the app; the access token identifies the signed-in user.
JWT signature is invalid
Verify SUPABASE_JWT_SECRET exactly matches the project's JWT secret. Rotating the secret requires restarting API instances and forcing users to sign in again.
issuer mismatch
Confirm SUPABASE_URL uses the same project that issued the token. Mixing staging dashboard config with production API config commonly causes this.
User signs in but sees no organization
Seed orgs, org_members, and user_current_org for the Supabase user ID. Email alone is not enough; the API keys membership by user ID.
RLS errors in Supabase
PandaStack application tables should live in the application database controlled by the API. If you place them inside Supabase Postgres and enable RLS manually, create explicit policies for the API service role or disable RLS on PandaStack-owned tables.
Dashboard has Supabase config but API still accepts local token
Check PANDASTACK_AUTH_MODE. It must be supabase on the API. Stub auth is only for single-tenant local development.
Invitations do not work
Confirm org_invites exists, the inviter is an owner or admin, and the dashboard redirect URL is allowed in Supabase.