# Authentication and User Management | Volcano

URL: https://volcano.dev/features/authentication

> Authentication on Volcano: email and password with verification and reset, OAuth with Google, GitHub, Microsoft and Apple, anonymous users that convert, hosted login pages, revocable sessions, and tokens your Postgres policies already trust.

## Authentication your database understands

Email and password, four OAuth providers and anonymous users, issuing one session that your Postgres policies, functions and storage rules all trust.

```console
$ volcano cloud config pull --force
# the auth block it writes out, edited to taste
auth:
  signup:            { enable_signup: true, enable_anonymous_signins: true }
  email_verification: { require_confirmation: true }
  managed_pages:     { enabled: true }
$ volcano cloud config deploy
✓ auth configured · hosted login and reset pages live
# auth.uid() now returns a real user inside every policy
```

## What happens after the token is issued

Plenty of services will verify a password and hand back a token. The token then means nothing to your database, so you write a permission check in every handler and repeat it wherever data is read. One missed check in new code is a disclosure bug.

Volcano issues the session, and everything else reads it. The same access token sets auth.uid() inside a row-level policy, arrives in a function as a verified \_\_volcano\_auth object, and decides what a storage policy allows. You write the rule once, in SQL.

## The same token, three enforcement points

*Identity*

Volcano installs auth.uid(), auth.email(), auth.role() and auth.is\_authenticated() into every database, hands functions a verified identity on the event, and exposes the same helpers to storage policies. Authorization gets written once, in the layer that holds the data.

- Postgres: policies filter rows with auth.uid()
- Functions: event.\_\_volcano\_auth carries user\_id and role
- Storage: the same helpers decide who can read a path

```console
# access token for user 8f1c…
postgres   auth.uid() → 8f1c…      12 of 1284 rows
function   __volcano_auth.role → authenticated
storage    8f1c/report.pdf allowed · 9a2b/… refused
no permission code in between
```

## Let people work before they sign up

*Anonymous*

Turn anonymous sign-ins on and a visitor gets a real user id immediately, covered by the same policies as everyone else. Converting to a permanent account keeps that id, so the work they already did stays theirs.

- Opt-in, off until you enable it
- Same row-level policies as signed-up users
- convertAnonymous keeps the original user id

Image: Auth settings with the anonymous sign-in switch and token lifetimes.

## Sessions you can list and end

*Sessions*

Access tokens are short-lived JWTs, and refresh tokens are held server-side, which is what makes them revocable. A user can see their devices and sign out one of them or all the others.

- Access token 3600s default, 60s to 86400s
- Refresh token 30 days, configurable to 90
- Refresh reuse interval of 10 seconds

```console
ses_91f4   macbook · safari      current
ses_2c07   iphone · app          2 days ago
ses_74be   unknown · chrome      3 weeks ago
$ await volcano.auth.deleteAllOtherSessions()
✓ 2 sessions revoked server-side
```

## What authentication covers

- **Email and password**: Signup, email verification, password reset by emailed link, and password change, each configurable per project.
- **OAuth providers**: Google, GitHub, Microsoft and Apple, with linking and unlinking on an account that already exists.
- **Anonymous users**: Opt-in sessions with a real user id that survives conversion into a permanent account.
- **Hosted pages**: Login, password reset and device approval pages served for your project, with HTML and CSS you can replace.
- **Identity in Postgres**: auth.uid(), auth.email(), auth.role() and auth.is\_authenticated() are installed in every database.
- **Session control**: List sessions, delete one, or delete all the others, with refresh tokens that rotate on use.

## Sign in, then let the policies do the work

### Client

```typescript
import { VolcanoAuth } from '@volcano.dev/sdk';

const volcano = new VolcanoAuth({
  apiUrl: process.env.NEXT_PUBLIC_VOLCANO_API_URL!,
  anonKey: process.env.NEXT_PUBLIC_VOLCANO_ANON_KEY!,
});

const { confirmationRequired } = await volcano.auth.signUp({ email, password });
if (!confirmationRequired) {
  const { session, error } = await volcano.auth.signIn({ email, password });
}

// OAuth redirects the browser, so these return nothing useful to await.
volcano.auth.signInWithGoogle();
volcano.auth.signInWithOAuth('github');

// Start without an account, convert later, keep the same user id.
await volcano.auth.signInAnonymously();
await volcano.auth.convertAnonymous({ email, password });

const unsubscribe = volcano.auth.onAuthStateChange((user) => {
  if (!user) redirect('/login');
});
```

### Policy

```sql
-- Installed in every Volcano database, no setup required:
--   auth.uid()               → the signed-in user's id, or NULL
--   auth.email()             → their email
--   auth.role()              → 'authenticated', 'anonymous' for an anonymous
--                              sign-in, or 'anon' with no session at all
--   auth.is_authenticated()  → auth.uid() is not null
--
-- TO authenticated covers registered and anonymous sign-ins alike. Add
-- auth.role() = 'authenticated' where a policy needs a registered account.

alter table documents enable row level security;

-- Owners read their own documents.
drop policy if exists documents_owner_read on documents;
create policy documents_owner_read on documents
  for select to authenticated using (owner_id = auth.uid());

-- Workspace members read them too. Policies on the same operation are OR'd
-- together, so a new rule is an addition rather than a rewrite.
drop policy if exists documents_workspace_read on documents;
create policy documents_workspace_read on documents
  for select to authenticated using (exists (
    select 1 from workspace_members m
    where m.workspace_id = documents.workspace_id
      and m.user_id = auth.uid()
  ));
```

### Function

```javascript
exports.handler = async (event) => {
  const auth = event.__volcano_auth;

  // Absent for anon-key callers, populated for a user session, and stripped
  // if a client tries to send its own. There is no token to parse here.
  if (!auth) {
    return { statusCode: 401, body: JSON.stringify({ error: 'sign in first' }) };
  }

  return {
    statusCode: 200,
    body: JSON.stringify({
      userId: auth.user_id,
      email: auth.email,
      role: auth.role,
      projectId: auth.project_id,
    }),
  };
};
```

### Configure

```yaml
version: 1

auth:
  signup:
    enable_signup: true
    enable_anonymous_signins: true

  email_verification:
    require_confirmation: true

  # Access token 60s to 86400s. Refresh token 1 hour to 90 days.
  tokens:
    access_token_lifetime: 3600
    refresh_token_lifetime: 2592000

  providers:
    email_password:
      enabled: true
    oauth:
      - provider: google
        enabled: true
        client_id: ${GOOGLE_CLIENT_ID}
        client_secret: ${GOOGLE_CLIENT_SECRET}   # never written to the file

  # Volcano serves login, password-reset and device-approval pages for this
  # project. Their HTML and CSS can be replaced, as can the email templates.
  managed_pages:
    enabled: true
    redirects:
      allowed: [https://app.acme.com/auth/callback]
      post_auth: https://app.acme.com/dashboard

# Declared lists are the source of truth: drop a provider from oauth and the
# next deploy removes it. volcano cloud config deploy --dry-run shows the diff.
```

## What people build with it

### Per-user data

Sign in with email or a provider, then let one policy per table decide what belongs to whom. The client can query the database directly because the rules do not live in the client.

### Multi-tenant SaaS

Membership tables plus policies give you tenant isolation the database enforces, so a missing WHERE clause in new code cannot leak across customers.

### Trials without an account

Anonymous users can start immediately. When they convert, the user id survives, so the document they created during the trial is still theirs.

## Works with the rest of Volcano

- [Row-level security](https://volcano.dev/features/postgres): auth.uid() and auth.role() are installed in every database, so a policy can be written on day one.
- [Identity in handlers](https://volcano.dev/features/edge-functions): Functions receive \_\_volcano\_auth already validated, so there is no token verification to get wrong.
- [Storage policies](https://volcano.dev/features/file-storage): Storage rules read the same helpers, so a browser can upload directly without a signing service.
- [Protected routes](https://volcano.dev/features/nextjs-hosting): Middleware helpers validate the session before a Next.js route renders, and redirect when there is none.

## Frequently asked questions

### Which sign-in methods are supported?

Email and password with optional verification and reset by emailed link, OAuth with Google, GitHub, Microsoft and Apple, and opt-in anonymous users. Magic links, SMS codes, passkeys, TOTP and SAML are not supported.

### How does the session reach my database?

Volcano passes the signed-in user into Postgres before your statement runs, which is what makes auth.uid(), auth.email() and auth.role() return real values inside a policy. The rule is written once in SQL, and every caller is held to it.

### How long do tokens last?

The access token is a JWT valid for 3600 seconds by default and configurable from 60 seconds to 86400. Refresh tokens default to 30 days, range from one hour to 90 days, and rotate with a 10-second reuse interval.

### Can I use my own login screen?

Yes. Build it against signIn, signUp, signInWithOAuth, forgotPassword and resetPassword, take the hosted pages as they are, or keep the hosted flow and replace its HTML and CSS.

### What is the difference between the anon key and the service key?

The anon key identifies the project and is safe to ship in a browser, with the user's JWT alongside it. The service key acts as service\_role, bypasses row-level security, and belongs on a server only.

### How do I connect my own tables to a user?

Store the id that auth.uid() returns. A user\_id uuid column plus a policy comparing it to auth.uid() is the whole join, and the same id reaches functions as \_\_volcano\_auth.user\_id and storage policies as auth.uid().

## Get started

Ready to add authentication? Create a free account at https://volcano.dev/signup, or have your coding agent follow the setup playbook at https://volcano.dev/setup.md.
