Web field Plate 26 Route §26
Plate 26

The API with accounts

You are holding the password now. That is one hash, one reset flow, one rate limit on the login route, and one decision you cannot take back quietly.

signup and login are route handlers, not a framework — they take your table and your field names and hand you the user. What you do with the user is yours: the token below is a JWT, but a database-backed revocable session from @atlas/security is the better default the moment you need to kick someone out.

token.sign and token.verify are async. Forgetting the await yields a Promise that stringifies to [object Promise] and a token that verifies as garbage.

What you are carrying

@atlas/server

Routes and the pipe pipeline.

@atlas/db

The users table.

@atlas/auth

Hashing, JWTs, signup and login flows.

@atlas/security

Rate limits, audit log, revocable sessions, TOTP.

@atlas/email

The reset mail you will need by week two.

Start it

atlas init -n myapi --template api

What it looks like

src/auth.tsts
import { token, signup, login, requireAuth } from "@atlas/auth"
import { post, pipeline, parseJson, json } from "@atlas/server"

export const routes = [
  post("/auth/signup", pipeline(parseJson)(signup({
    db, table: "users", fields: ["email", "name", "password"],
    onSuccess: (c, user) => json(c, 201, { id: user.id, email: user.email }),
  }))),

  post("/auth/login", pipeline(parseJson)(login({
    db, table: "users", identity: "email", password: "password",
    // token.sign is async — the missing await is the bug you will not see.
    onSuccess: async (c, user) =>
      json(c, 200, { token: await token.sign({ id: user.id }, secret) }),
  }))),
]

export const guard = requireAuth({ secret })

Read next

@wess/atlas/auth @wess/atlas/security Cookbook
Type to search guides and package references.