Skip to main content

Authentication

info

Authentication is not implemented by this API. We delegate the entire login / signup / token-refresh flow to Supabase Auth (GoTrue). Our server only verifies the bearer JWT on each request.

How it works​

  1. Client signs in directly against Supabase Auth using the Supabase JS SDK (or the GoTrue REST API). The client never hits our API for credentials.
  2. Supabase returns a JWT (the access_token) plus a refresh_token.
  3. Client calls our API with Authorization: Bearer <access_token>.
  4. Our API verifies the token via supabase.auth.getUser(token) and attaches the resolved Supabase User to the Hono context as c.get("user").

The server-side verification middleware lives in src/core/middleware/auth.middleware.ts:

const authHeader = c.req.header("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw new UnauthorizedHTTPException();
}
const token = authHeader.slice(7);
const { data, error } = await supabase.auth.getUser(token);
if (error || !data.user) {
throw new UnauthorizedHTTPException();
}
c.set("user", data.user);

Client integration​

TypeScript / browser​

import { createClient } from "@supabase/supabase-js";

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

// Sign up
const { data, error } = await supabase.auth.signUp({
email: "user@example.com",
password: "securePassword123",
});

// Sign in
const { data: session, error: signInErr } =
await supabase.auth.signInWithPassword({
email: "user@example.com",
password: "securePassword123",
});

// Use the token against our API
const accessToken = session.session?.access_token;
await fetch("/profiles", {
headers: { Authorization: `Bearer ${accessToken}` },
});

The Supabase SDK handles refresh-token rotation automatically.

REST (no SDK)​

POST /auth/v1/signup HTTP/1.1
Host: <SUPABASE_URL>
apikey: <SUPABASE_ANON_KEY>
Content-Type: application/json

{ "email": "user@example.com", "password": "securePassword123" }
POST /auth/v1/token?grant_type=password HTTP/1.1
Host: <SUPABASE_URL>
apikey: <SUPABASE_ANON_KEY>
Content-Type: application/json

{ "email": "user@example.com", "password": "securePassword123" }

GoTrue REST docs: supabase.com/docs/reference/javascript/auth-api

Bearer header contract​

Every authenticated endpoint on this API expects:

Authorization: Bearer <supabase-access-token>
FailureResponse
Missing header401 Unauthorized
Header is not Bearer …401 Unauthorized
Token rejected by supabase.auth.getUser()401 Unauthorized

Errors are JSON: { "error": "Unauthorized", "errorId": "uuid" } (see src/libs/errors.ts).

Token TTLs​

These come from the Supabase project's auth settings. Defaults:

TokenDefault TTL
access_token1 hour
refresh_tokenLong-lived, rotates on use

Configure in Supabase Dashboard → Auth → Sessions.

Local development​

For the local self-hosted Supabase stack, the auth endpoint is http://localhost:8000/auth/v1/... (Kong → GoTrue). The dev script api/devtools/authenticate.ts mints a local access token interactively for use in *.http files:

bun run ./devtools/authenticate.ts

It prints the JWT to stdout; paste it into your editor's REST client.

Error responses​

Validation errors use { "error": "Validation has failed", "fields": [...] }. HTTP errors use { "error": "...", "errorId": "uuid" }; an optional code is included for machine-readable conflicts. GET /health is public and returns { "status": "ok" }. The generated OpenAPI document is at /openapi.json, with Swagger UI at /docs.

What this API does not do​

  • ❌ /auth/login, /auth/register, /auth/refresh, /auth/verify — call Supabase directly.
  • ❌ Password hashing, email verification, magic-link emails — Supabase.
  • ❌ OAuth provider redirects (Google, Apple, etc.) — Supabase.

What this API does do​

  • ✅ Verify the JWT on every authenticated route.
  • ✅ Look up application-level user state (profile, friendships, etc.) keyed by the Supabase user ID.
  • ✅ Apply business-rule authorization on top of the verified identity.