# Authentication Form-based authentication: two pages, `/signin` and `/signup`, sharing one schema and one set of rpc handlers. Credentials store as bcrypt hashes in Postgres. Each rpc validates against the hash and calls `session.login()`. The reactive session updates both pages immediately on success, no full-page reload. The Elements schema setup automatically installs the `pgcrypto` extension, so `crypt()` and `genSalt()` are available in migrations and rpc. ## Migration ```bash elements create migration 'add users' -tables=users ``` `app/migrations/-add-users.migration.sql`: ```sql -- add users -- Auto-update updatedAt on row changes. create or replace function touchUpdatedAt() returns trigger language plpgsql as $$ begin new.updatedAt = now(); return new; end; $$; create table users ( id uuid primary key default uuidGenerateV7(), createdAt timestamptz not null default now(), updatedAt timestamptz not null default now(), handle text not null unique, passwordHash text not null ); create trigger usersTouchUpdatedAt before update on users for each row execute function touchUpdatedAt(); ``` The two columns added on top of the scaffold are `handle` (unique, so no two users share one) and `passwordHash` (stores the bcrypt hash, never the plaintext). ## Shared auth rpc `app/shared/services/auth.ts`: ```ts import { sql, session, AuthError } from "@elements/app"; interface User { id: string; handle: string; } /** @rpc */ export function signinUser(handle: string, password: string) { let user = sql( `select id, handle from users where handle = ${handle} and passwordHash = crypt(${password}, passwordHash)`, ).first(); if (!user) { throw new AuthError("invalid handle or password"); } session.login({ userId: user.id, userName: user.handle }); } /** @rpc */ export function signupUser(handle: string, password: string) { let user = sql<{ id: string }>( `insert into users (handle, passwordHash) values (${handle}, crypt(${password}, genSalt('bf', 12))) returning id`, ).firstOrThrow(); session.login({ userId: user.id, userName: handle }); } /** @rpc */ export function logoutUser() { session.logout(); } ``` The signin query reads `passwordHash = crypt(${password}, passwordHash)`. Both occurrences are the same column. `crypt()` re-hashes the submitted password using the algorithm and salt encoded in the stored hash and compares the result, all in one expression. The plaintext never leaves Postgres and TypeScript never sees the stored hash. Signup uses `genSalt('bf', 12)` to mint a fresh bcrypt salt for the new user. `bf` is bcrypt; pick a different scheme only if you understand the trade-off. **Pass the cost.** The second argument is the work factor, and it is the whole point of bcrypt: it is how long one hash takes, and therefore how long one guess takes for someone holding a stolen `users` table. pgcrypto defaults `bf` to 6, which is far too cheap on modern hardware. Measured on the bundled Postgres: ```text crypt('x', gen_salt('bf')) 4.1 ms cost 6, the default crypt('x', gen_salt('bf', 12)) 235 ms cost 12 ``` That is 57x more work per guess. Cost 12 is the right default today. Raise it as hardware gets faster; the cost is stored in the hash, so old rows keep verifying at the cost they were written with and you can re-hash on next signin. `AuthError` is a safe error: its message reaches the browser as-is and the rpc client re-throws it. ## Declaring session fields `session.login({ userId, userName })`, `session.get('userId')`, and `session.getOrThrow('userName')` only typecheck once you declare those fields on `SessionData`. Do it once per app in `app/types/session.d.ts`. The scaffold ships this file with the block commented out: ```ts declare module "@elements/app" { interface SessionData { userId: string; userName: string; } } ``` `SessionData` is a global augmentation: declare it once and every `session.login`/`get`/`getOrThrow` across the app is typed against it. Without it, `keyof SessionData` is empty, so `session.login({ userId })` reports "expected 0 arguments" and `session.get('userId')` rejects the key. ## Signin page ```bash elements create page signin ``` Update `app/pages/signin/index.ts` and `template.html` with the contents below. `app/pages/signin/index.ts`: ```ts import signin from "#app/pages/signin/template"; export default function route(req, res) { return new signin(); } ``` `app/pages/signin/template.html`: ```html import "./style.css"; import { session } from "@elements/app"; import { signinUser, logoutUser } from "#app/shared/services/auth"; function attemptSignin(handle: string, password: string): string { try { signinUser(handle, password); return ""; } catch (err: any) { return err.message; } }

welcome, {session.get('userName')}

{ error = attemptSignin(handle, password); if (!error) { handle = ""; password = ""; } }}>

sign in

{error}

need an account? sign up
``` ## Signup page ```bash elements create page signup ``` Update `app/pages/signup/index.ts` and `template.html`. `app/pages/signup/index.ts`: ```ts import signup from "#app/pages/signup/template"; export default function route(req, res) { return new signup(); } ``` `app/pages/signup/template.html`: ```html import "./style.css"; import { session } from "@elements/app"; import { signupUser, logoutUser } from "#app/shared/services/auth"; function attemptSignup(handle: string, password: string): string { try { signupUser(handle, password); return ""; } catch (err: any) { return err.message; } }

welcome, {session.get('userName')}

{ error = attemptSignup(handle, password); if (!error) { handle = ""; password = ""; } }}>

sign up

{error}

have an account? sign in
``` `session.isLoggedIn()` is reactive in the browser. The moment a signin or signup rpc returns successfully, the welcome branch renders without a page reload. Either page works as a deep link: the user lands on `/signin` or `/signup` directly and follows the link if they picked the wrong one. ## Routes Register the pages in `index.ts` alongside the scaffold's existing structure: ```ts import signin from "#app/pages/signin"; import signup from "#app/pages/signup"; // ... app.route("/signin", signin); app.route("/signup", signup); ``` ## Notes - `session.login({ userId, userName })`. `userId` is required and marks the session as logged in. `userName` is the display name. For this recipe the two are different: id is a uuid, the handle is human-readable. - `crypt()` and `genSalt()` come from the `pgcrypto` extension. Elements installs the extension at project setup, so they're always available. - The unique constraint on `handle` means a signup with a taken name throws a `SqlError`. The current template surfaces the raw Postgres message. For a friendlier UX, catch in the rpc and rethrow with a clearer `AuthError("that handle is taken")`. - Session expiry is an app-wide policy set by `session.expires` in `config.jsoc` (the scaffold sets `'30d'`). It applies to every session; `session.login()` takes only the session data (`userId`, `userName`, and any fields you declare on `SessionData`). There is no per-login expiry override.