Source: https://www.hype-stack.dev/docs/backend

# Backend Overview

The backend lives in `apps/backend/` and runs a Hono API server on Node.js. It uses Prisma for schema management, Kysely
for typed queries, PostgreSQL for storage, and Redis (Valkey) for caching.

## Folder structure

```
apps/backend/src/
  cache/         # Valkey (Redis) client and initialization
  config/        # Environment config, app constants
  db/            # Database client (Postgres + Kysely)
  features/      # Domain-organized business logic
  jobs/          # Registered cron jobs (empty until a pack adds one)
  libs/          # Shared backend libraries: email, logger, scheduler, sentry, storage, websocket
  middleware/    # Error handling, validation, auth, file uploads
  paraglide/     # Compiled backend message catalog (emails, notifications)
  routes/        # HTTP route definitions (thin layer)
  sockets/       # WebSocket event definitions
  testing/       # Test utilities, mocks, setup
  utils/         # Shared helpers
```

## Bootstrap order

At process start the server roughly does this:

**Startup**

1. **Validate env** — Zod parses process.env. Missing keys stop the process before ports open.
2. **Attach context** — Postgres, Valkey, storage, and other clients land on the Hono app context.
3. **Register sockets and routes** — WebSocket routers and HTTP routers mount under their prefixes.
4. **Global middleware** — CORS and the centralized error middleware wrap every request.
5. **Start the scheduler** — Once the database is reachable, every job in src/jobs runs on its cron schedule.

## Request lifecycle

**Authenticated HTTP request**

1. **Global middleware** — CORS runs, then the error middleware wraps the rest of the chain.
2. **Route match** — Hono picks the feature router mounted from registerRoutes.
3. **Auth and permissions** — getAuthenticatedApp() runs withAuthUser. Routes add hasAccess({ action, subject }) when needed. (After a starter pack)
4. **Validation** — The validate middleware parses json or query input with Zod and types the handler.
5. **Module then db** — The thin route calls a feature module. The module calls queries or mutations. No business logic in the route file.
6. **JSON response or typed error** — Success returns ctx.json(...). Failures throw ApplicationError / AuthError / ValidationError and the error middleware formats the response (and reports unexpected errors to Sentry when configured).

## Layer responsibilities

| Layer                         | Responsibility                                 | Must not do                             |
| ----------------------------- | ---------------------------------------------- | --------------------------------------- |
| `main` / app bootstrap        | env, context, mount routers, global middleware | Own feature rules                       |
| Global middleware             | CORS, centralized errors                       | Feature-specific DB writes              |
| Route middleware              | Auth, `validate()`, `hasAccess()`, uploads     | Orchestrate multi-step business flows   |
| Route handler                 | Read context, call one module, return JSON     | Raw SQL or provider SDK sprawl          |
| Feature module                | Business rules and orchestration               | Know HTTP status codes                  |
| `db/queries` / `db/mutations` | Persistence                                    | Session / request semantics beyond args |

## Key patterns

- **Features own business logic.** Each domain (projects, notifications, auth) has its own folder in `src/features/`
  with modules, database queries, mutations, and schemas. See
  [How to organize features](/docs/backend/organize-features).

- **Routes are thin.** Route files validate input, read context, call a module function, and return a response. No
  business logic in routes. See [Routing](/docs/backend/routing).

- **No try/catch.** A centralized error middleware handles all errors. Throw typed errors (`ApplicationError`,
  `AuthError`, `ValidationError`) and let the middleware do its job. Swallowing errors also skips Sentry.

- **Validation goes through Zod.** Every route that accepts input uses the `validate()` middleware with a Zod schema.
  This powers both runtime validation and the end-to-end type bridge to the frontend SDK.

## Scheduled jobs

The backend runs its own cron scheduler (croner) in process, so there is no worker to deploy. Jobs are declared in
`src/jobs/index.ts` and start with the server:

```ts
import type { JobDefinition } from "../libs/scheduler/scheduler";

export const jobs: JobDefinition[] = [{ name: "newsletter-dispatch", cron: "* * * * *", run: dispatchDueReleases }];
```

Each fire takes a Postgres advisory lock, so a job runs once across every instance of the API. Jobs are stateless
sweepers: they look at your tables for work that is due and do it, and a crashed run is simply picked up on the next
tick. A job holds a database transaction while it runs, so keep it fast. If it finds slow work, it enqueues
[tasks](#task-queue) and returns. The list ships empty. Feature packs append entries through the `jobs` field of their
manifest, and your own jobs go in the same array.

## Task queue

Cron answers "when". The task queue answers "how much at once". Anything slow (parsing an upload, calling a model,
sending a batch) runs as a task, so it can retry on its own and never holds up a request or a cron tick.

Tasks live in Postgres (pg-boss, in its own `pgboss` schema), so there is still nothing extra to deploy. Queues are
declared with `defineQueue` and registered in `src/queues/index.ts`:

```ts
export const renderReportQueue = defineQueue({
  name: "render-report",
  schema: z.object({ reportId: z.string() }),
  concurrency: 2,
  exclusive: true,
  run: async ({ data, signal }) => renderReport(data.reportId, signal),
  onExhausted: async ({ data }) => markReportFailed(data.reportId),
});
```

Enqueue inside the transaction that writes the row. The task commits or rolls back with it, so a row can never sit in
`queued` with nothing behind it:

```ts
await postgres.qb.transaction().execute(async (trx) => {
  await trx.insertInto("report").values(report).execute();
  await enqueue(renderReportQueue, { reportId: report.id }, { trx, key: report.id });
});
```

A failed run is retried with backoff (3 retries by default), then `onExhausted` fires once so you can mark your own row
as failed. Handlers can run twice after a crash or a deploy, so checkpoint progress in your tables and resume from it.
Keep a status column the UI reads; the queue is plumbing. The list ships empty. Feature packs append entries through the
`queues` field of their manifest.

### Where tasks run

Inside the API process, next to the scheduler. There is no separate worker to deploy. Every backend instance works
tasks, `concurrency` at a time per queue, so adding a replica adds capacity. The queue is free until used: with nothing
registered in `src/queues/index.ts` it creates no schema and polls nothing. When it is on, it runs on the backend's own
database connections, so there is no second pool.

A dedicated worker is not something the template ships. If a project ever needs one, the seam is already there:
`main.ts` starts the queue with `startQueues({ queues, postgres })`, so a second entry file can make the same call after
`setupContext()`, with `workers: false` passed in `main.ts`. Tasks live in Postgres, so the two processes never need to
talk to each other.

## HyperFetch SDK generation

`registerRoutes` returns a typed Hono app. The frontend HyperFetch SDK is derived from that type, so a new route appears
on `sdk.*` without a separate codegen step. Details: [end-to-end type safety](/docs/development/e2e-typesafety) and
[HTTP](/docs/frontend/http).

## Database access

- **Prisma** for schema definition, migrations, and seeding
- **Kysely** for typed SQL queries (reads and complex writes)
- **Prisma for inserts**, Kysely for everything else

Reads go in `db/queries/`, writes go in `db/mutations/`. See [Prisma and Kysely](/docs/backend/prisma-kysely) and
[Migrations](/docs/backend/migrations).
