# {{projectName}} / {{appName}}

Voltro backend scaffold (template: **api-auth**).

Real user authentication, wired turnkey with
[`@voltro/plugin-auth`](https://voltro.cloud/docs/plugins/auth): password
**sign-up / sign-in / sign-out** over HttpOnly **session cookies**, a strategy
that resolves the session into a typed `Subject` on every rpc call, and a
`session.me` action that proves the loop end-to-end. Boots with **zero infra**
(`memoryUserStore()`).

## Boot

```bash
pnpm install
pnpm --filter @{{projectName}}/{{appName}} dev
# → http://localhost:4000   (rpc + /auth/* HTTP routes)
```

The shipped `.env` supplies a DEV-ONLY `VOLTRO_SESSION_SECRET` so it boots out
of the box. **Replace it before production** (`openssl rand -hex 32`) — see
Environment below.

## The two pieces

`app.config.ts` wires both halves with ONE shared secret:

```ts
plugins: [
  authRoutesPlugin({ store: memoryUserStore(), secret: SECRET, defaultTenantId: 'acme' }),
],
auth: {
  strategies: [voltroPasswordStrategy({ secret: SECRET })],
},
```

- **`authRoutesPlugin`** mounts the HTTP auth surface under `/auth/*` and
  **signs** the session cookie on success.
- **`voltroPasswordStrategy`** runs in the AuthMiddleware chain on every
  rpc/ws call, **verifies** that cookie, and resolves it to a `Subject` — so
  handlers read the user via `ctx.request.subject`.

They MUST share the same `secret` (one signs, the other verifies). The template
reads `VOLTRO_SESSION_SECRET` once and passes it to both.

## Try the loop (curl)

State-changing routes are CSRF-protected, so grab a token first. (`session.me`
is an rpc procedure — invoke it over HTTP via the dev inspect endpoint, which
forwards your session cookie; from a browser you'd use `useAction('app',
'session.me')`.)

```bash
# 1. Get a CSRF token (also sets the csrf cookie into the jar)
curl -s -c jar.txt http://localhost:4000/auth/csrf
#    → { "csrfToken": "<csrf>" }

# 2. Sign up (sets the HttpOnly session cookie into the jar)
curl -s -b jar.txt -c jar.txt -X POST http://localhost:4000/auth/sign-up \
  -H 'content-type: application/json' -H 'x-csrf-token: <csrf>' \
  -d '{"email":"ada@example.com","password":"hunter2hunter2"}'
#    → { "ok": true, "subject": { "type": "user", "id": "u_…", "tenantId": "acme" } }

# 3. Call session.me, forwarding the session cookie → you're a `user`
curl -s -b jar.txt -X POST http://localhost:4000/_voltro/inspect/invoke \
  -H 'content-type: application/json' \
  -d '{"tag":"session.me","input":{}}'
#    → { "ok": true, "result": { "type": "user", "id": "u_…", "tenantId": "acme" } }
```

Call `session.me` WITHOUT the cookie and `type` is `"anonymous"` with a null
`id` — that's the strategy resolving (or not resolving) the session. (Note: the
session cookie is `Secure` only in production — see `cookieSecure` in
`app.config.ts` — so the loop works over plain-HTTP `localhost` in dev.)

`/auth/*` also exposes `sign-in`, `sign-out`, `password-reset`,
`magic-link`, session management (`/auth/sessions`), and tenant memberships —
see the plugin docs.

## Environment

| Var | Access | Why |
|---|---|---|
| `VOLTRO_SESSION_SECRET` | secret | HMAC key signing + verifying the session cookie. The shipped `.env` is a DEV-ONLY placeholder; generate a real one (`openssl rand -hex 32`) and set it in your deployment's secrets. Rotating it invalidates all existing sessions. |
| `LOG_LEVEL` | public | `debug \| info \| warn \| error` (default `info`). |

## Files

```
app.config.ts                 authRoutesPlugin + voltroPasswordStrategy + memoryUserStore
.env                          DEV-ONLY VOLTRO_SESSION_SECRET
database/schema.ts            core actors + tenants (users live in the memory store)
actions/
  me.action.ts(.server)       session.me — returns the resolved Subject
```

## Going to production

- **Durable accounts** — swap `memoryUserStore()` for `postgresUserStore({ sql })`
  and `store: 'postgres'`. The postgres store manages its own auth tables
  (auto-migrated).
- **Email flows** — pass `sendEmail: mailSender(mailService)` (from
  `@voltro/plugin-mail`) to `authRoutesPlugin` so magic-link + password-reset
  can deliver. Without it those routes still mint tokens but return `202`.
- **Cookie hardening** — set `cookieSecure: true` (and `cookieDomain`) for
  HTTPS deployments.
- **Pair with a frontend** — `frontend-app` shows the reactive client; add a
  login form that POSTs to `/auth/sign-in`, and authenticated rpc calls carry
  the session cookie automatically.
