Auth guides
02 · Quickstart

Wire signup + signin into your product.

Provision an app from the console, hit signup, signin, and /me from a test client. By the end you have one working end-user authenticating against your app, with a JWT you can verify in your own backend.


Step 1

Create an app in the console

From console.productcraft.co/auth, hit + New app and fill in the Create new app dialog. Pick a slug — it appears in every Consumer-API URL for the rest of this guide.

Treat the slug as permanent: it's baked into every Consumer-API URL and into the iss / aud claims of every token the app issues, so renaming it later breaks outstanding tokens and every integration URL. Change the display name freely, but pick a slug you can live with. For this guide we'll use acme. After creation the console drops you on the app's Settings tab, which shows the slug and app id together (/acme · <app-id>) — that's where you copy them from later.

Every new app comes with three seeded end-user roles (owner, admin, member); new signups get member by default. Chapter 3 covers customising them.

Prefer the API? App creation is one call with a PAK (a pcft_live_* workspace API key minted in the console under Workspace → API keys New API key):

POST /v1/apps (Auth-admin)
curl https://api.auth.productcraft.co/v1/apps \
  -H 'authorization: Bearer pcft_live_...' \
  -H 'content-type: application/json' \
  -d '{
    "slug": "acme",
    "display_name": "Acme",
    "workspace_id": "<your-workspace-uuid>"
  }'

Step 2

Enable signup

New apps ship locked down: signup_enabled defaults to false, so POST /auth/signup returns 403 until you flip it on. Do that in the console under Settings → Auth config → Sign-in policy (the Signup enabled toggle, then Save sign-in policy), or with one API call.

PATCH /v1/apps/:appId/auth-config
curl -X PATCH https://api.auth.productcraft.co/v1/apps/<app-id>/auth-config \
  -H 'authorization: Bearer pcft_live_...' \
  -H 'content-type: application/json' \
  -d '{ "signup_enabled": true }'

The other defaults are sensible — leave them unless your product needs different behaviour:

  • signin_enabled (default true) — emergency switch: when false, signin returns 403 without revoking already-issued tokens.
  • Password policy — password_min_length (default 12), password_require_uppercase (default true), password_require_number (default true), password_require_symbol (default false).
  • session_duration_minutes (default 10080 — 7 days) — how long a session and its refresh token live. Access tokens always expire after 1 hour.
  • signup_default_role (default member) — the role new signups get.
  • enforce_app_permissions (default false) — when off, any valid token can call any Consumer-API route; when on, permission-gated routes check the user's role permissions. Keep this off until you've modelled your roles (Chapter 3).

Step 3

Sign up your first user

Public surface. No bearer needed. The password must pass the app's policy (by default 12+ characters with an uppercase letter and a number — violations return 400 with a message naming the failed rule). The response includes an access token + refresh token ready to use. Email is the primary contact and starts unverified — that's fine for the first call; Chapter 7 covers the verification flow.

POST /acme/v1/auth/signup
curl https://api.auth.productcraft.co/acme/v1/auth/signup \
  -H 'content-type: application/json' \
  -d '{
    "email": "ada@example.com",
    "username": "ada",
    "password": "CorrectHorse42Battery",
    "display_name": "Ada Lovelace"
  }'

Response:

{
  "access_token":  "eyJhbGc... (1h TTL)",
  "refresh_token": "eyJhbGc... (7d TTL by default)",
  "token_type": "Bearer",
  "expires_in": 3600
}

Step 4

Sign in an existing user

Same response shape as signup. identifier accepts either the username or the account's primary email — secondary email contacts do not authenticate.

POST /acme/v1/auth/signin
curl https://api.auth.productcraft.co/acme/v1/auth/signin \
  -H 'content-type: application/json' \
  -d '{
    "identifier": "ada",
    "password": "CorrectHorse42Battery"
  }'

Failure modes — 401 { statusCode:401, message:"Invalid credentials" } for a wrong password or an unknown identifier (Auth does not differentiate the two, to avoid a username-existence oracle). A suspended account returns 403 Account is suspended, an app with signin disabled returns 403, and too many failed attempts returns 429.


Step 5

Read the user's profile

With the access token from step 3 or 4, hit /me. This is the canonical 'who am I' endpoint.

GET /acme/v1/me
curl https://api.auth.productcraft.co/acme/v1/me \
  -H 'authorization: Bearer eyJhbGc...'
{
  "id": "648616c8-...",
  "username": "ada",
  "display_name": "Ada Lovelace",
  "role": "member",
  "joined_at": "2026-05-11T11:39:37Z",
  "created_at": "2026-05-11T11:39:35Z",
  "email": "ada@example.com",
  "email_verified_at": null
}

role is the user's role name on this app — by default signup_default_role (which defaults to member). To get the flat permission set bound to that role, hit GET /me/permissions — that resolves through a 60-second cache, so role-permission edits propagate within a minute.


Step 6

Refresh the access token

Access tokens expire after 1h. Use the refresh token to mint a fresh pair without re-asking for credentials. The refresh token is rotated on every call — the previous one stops working immediately, and re-using it after a short grace window (60s, to absorb network retries) is treated as theft and revokes the whole session.

POST /acme/v1/auth/refresh
curl https://api.auth.productcraft.co/acme/v1/auth/refresh \
  -H 'content-type: application/json' \
  -d '{ "refresh_token": "eyJhbGc... (the previous refresh)" }'

Step 7

Sign out

POST /auth/logout with the refresh token, or DELETE /me/sessions/:id with the bearer. Both revoke the session — the Consumer API rejects the access token immediately (401 'Session has been revoked'). Only your own services doing local JWKS verification keep accepting the JWT until its TTL.

POST /acme/v1/auth/logout (the simple path)
curl https://api.auth.productcraft.co/acme/v1/auth/logout \
  -H 'content-type: application/json' \
  -d '{ "refresh_token": "eyJhbGc..." }'

What's next

You have a working Auth integration

From here:

  • Chapter 3 — bind permissions to roles so your routes can say “only users with billing.read may call this.”
  • Chapter 6 — verify the JWT in your own backend, not by calling /me on every request.
  • Chapter 7 — wire up email verification so unverified emails don't accumulate forever.
  • The full API reference at /docs/auth/api-reference has every endpoint, including the ones we didn't touch here (contact management, session list, activity log, tenant switching).