Getting Started

Quick start

Start the dev server, create your first application, and perform an OAuth 2.1 authorization flow.

Quick start

This guide assumes you have completed the Installation steps and have both the database and backend running.

Start the backend

pnpm dev

The server starts with hot-reload (tsx watch) on http://localhost:3001.

Start the frontend (optional)

To run the Vue 3 admin SPA with Vite HMR:

cd frontend
pnpm dev

The Vite dev server starts on http://localhost:5173 and proxies API calls to localhost:3001.

For most admin tasks you can simply open http://localhost:3001 and use the pre-built SPA from frontend-dist/.

Bootstrap the superadmin

If ADMIN_EMAIL and ADMIN_PASSWORD are set in .env, a superadmin account is created automatically the first time the server starts:

[bootstrap] Superadmin created: admin@example.com

If those variables are absent, you must create the first user manually via the BetterAuth sign-up endpoint:

curl -X POST http://localhost:3001/api/auth/sign-up/email \
  -H "Content-Type: application/json" \
  -d '{"name":"Admin","email":"admin@example.com","password":"changeme123!"}'

Then promote the user to superadmin via the admin API (requires an existing admin session or direct DB update).

Register your first application

Applications are the OAuth 2.1 clients. Use the admin SPA or the REST API:

# Sign in first to get a session cookie
curl -c cookies.txt -X POST http://localhost:3001/api/auth/sign-in/email \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"changeme123!"}'

# Create an application
curl -b cookies.txt -X POST http://localhost:3001/api/admin/applications \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My App",
    "slug": "my-app",
    "redirectUris": ["http://localhost:8080/callback"],
    "allowedScopes": ["openid","profile","email","roles"]
  }'

The response includes clientId (equals the slug) and clientSecret. Store the secret — it is shown only once.

Perform an authorization flow

  1. Redirect the user's browser to the authorization endpoint:
http://localhost:3001/api/auth/oauth2/authorize
  ?response_type=code
  &client_id=my-app
  &redirect_uri=http://localhost:8080/callback
  &scope=openid%20profile%20email%20roles
  &code_challenge=<PKCE_S256_challenge>
  &code_challenge_method=S256
  &state=<random>
  1. The user signs in at /login, optionally approves the consent screen at /oauth2/consent, and is redirected back to redirect_uri with ?code=….
  2. Exchange the code for tokens:
curl -X POST http://localhost:3001/api/auth/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=authorization_code" \
  -d "code=<code>" \
  -d "redirect_uri=http://localhost:8080/callback" \
  -d "client_id=my-app" \
  -d "client_secret=<secret>" \
  -d "code_verifier=<PKCE_verifier>"

Response:

{
  "access_token": "...",
  "id_token": "...",
  "refresh_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Decode the id_token to find the custom claims:

{
  "sub": "<userId>",
  "email": "user@example.com",
  "roles": ["editor"],
  "permissions": ["articles", "comments.write"],
  "features": { "maxProjects": 10, "exportEnabled": true }
}
Copyright © 2026