Source: https://www.hype-stack.dev/docs/development/platform-map

# Platform map

A Hype Stack app already runs auth, permissions, feature flags, realtime, background jobs, a task queue, file storage,
and email. This page is the list of those pieces, what you import to use each one in your own feature, and where it gets
registered. Each section links to the page with the full details.

Everything here works the same in plain app code and inside a pack. The only difference is who does the registering: you
edit the registry file by hand, or a pack declares a manifest field and the CLI edits it for you.

## At a glance

| Mechanism                           | Use it for                                     | In your code                                                  | Pack manifest field |
| ----------------------------------- | ---------------------------------------------- | ------------------------------------------------------------- | ------------------- |
| [Request context](#request-context) | Who is calling, which org, what they may do    | `ctx.var.user`, `ctx.var.organizationId`, `ctx.var.ability`   | `context`           |
| [Routes](#routes)                   | An authenticated API under a prefix            | `getAuthenticatedApp()` in `routes/<domain>/index.ts`         | `routes`            |
| [Permissions](#permissions)         | Who may read, write, or manage a thing         | `hasAccess({ action, subject })`, `useUser().hasPermission`   | `permissions`       |
| [Feature flags](#feature-flags)     | Turning a feature on per organization          | `withFeature(key)`, `isFeatureEnabled`, `useFeatureFlag(key)` | none, edit catalog  |
| [Realtime](#realtime)               | Pushing an update to a user or a whole org     | `wsEmit.toUser`, `wsEmit.toOrganization`                      | `sockets`           |
| [Scheduled jobs](#scheduled-jobs)   | Work on a clock: reminders, sweeps, digests    | a `JobDefinition` in `src/jobs/index.ts`                      | `jobs`              |
| [Task queue](#task-queue)           | Slow or retryable work after a request returns | `defineQueue`, `enqueue(queue, data, { trx })`                | `queues`            |
| [File storage](#file-storage)       | Uploads: avatars, logos, attachments           | `uploadFile`, `getFileUrl`, `deleteFile`                      | none                |
| [Email](#email)                     | Transactional mail                             | `resend.emails.send(...)`                                     | none                |
| [Cache](#cache)                     | Short-lived shared state, rate limits          | `valkey` from `@backend/context`                              | none                |
| [App shell slots](#app-shell-slots) | Showing up in nav, menus, settings, admin      | entries in the `constants/*` files                            | `nav`, `*Actions`   |
| [Shared enums](#shared-enums)       | Types and constants both sides import          | `@<your-app>/enums`                                           | `enums`             |

## How one request uses them

**A typical write in a feature**

1. **withAuthUser** — Loads the session and sets user, organizationId, and ability on the context.
2. **hasAccess** — Stops the request with a 403 when the role cannot do this action on this subject.
3. **Feature module** — Runs the business logic and scopes every query by organizationId.
4. **Side effects** — Enqueues slow work in the same transaction, pushes a socket event, sends an email.

## Request context

Every route built on `getAuthenticatedApp()` runs `withAuthUser` first. After it, `ctx.var` holds:

| Variable         | What it is                                                                   |
| ---------------- | ---------------------------------------------------------------------------- |
| `user`           | The signed-in user                                                           |
| `organizationId` | The active organization. Treat it as the tenant id for every query           |
| `ability`        | A CASL ability built from the member's role. See [Permissions](#permissions) |
| `sessionId`      | The current session record                                                   |

Shared clients live in `apps/backend/src/context.ts`: `postgres` (Kysely on `postgres.qb`), `valkey`, and `storage`.
Import them directly. A starter or pack adds its own client the same way (`auth`, `workos`, `stripe`), which is what the
`context` manifest field does.

Never take an organization id from the request body or query string. Read it from `ctx.var.organizationId`, which the
middleware already checked against the user's membership.

## Routes

A feature's routes are one Hono app per domain. Build it on `getAuthenticatedApp()` so the auth middleware runs, and
keep the logic in `features/<domain>/modules/`:

```ts
// apps/backend/src/routes/invoices/index.ts
import { listInvoices } from "@backend/features/invoices/modules/list-invoices";
import { hasAccess } from "@backend/middleware/permissions/has-access";
import { getAuthenticatedApp } from "@backend/utils/auth/auth-hono";

export const invoiceRoutes = getAuthenticatedApp().get(
  "/",
  hasAccess({ action: "read", subject: "invoice" }),
  async (ctx) => {
    const { organizationId } = ctx.var;
    return ctx.json(await listInvoices({ organizationId }));
  },
);
```

Then chain `.route("/invoices", invoiceRoutes)` onto the app in `registerRoutes` (`apps/backend/src/routes/index.ts`)
and run `pnpm generate`. The frontend SDK picks the new endpoints up from the exported route types.

- Folder layout: [Organize features](/docs/backend/organize-features)
- Route conventions and validation: [Routing](/docs/backend/routing)
- Calling it from React: [HTTP](/docs/frontend/http)

## Permissions

Roles map to permission strings like `read:organization` or `manage:project`. The backend checks them with `hasAccess`,
the frontend with `useUser().hasPermission`. A new feature adds its own subject and grants it to roles.

That is its own page: [Access control](/docs/backend/access-control).

## Feature flags

SaaS starters ship per-organization flags. A flag is an entry in `packages/enums/src/feature-flags/catalog.ts`, and the
admin app gets a Flags tab to switch it per org.

```ts
// backend: gate a whole route
app.get("/reports", withFeature("beta-reports"), handler);

// backend: branch inside a module
const enabled = await isFeatureEnabled({ organizationId, key: "beta-reports" });
```

```tsx
// frontend, mobile, extension
const enabled = useFeatureFlag("beta-reports");

<FeatureGate flag="beta-reports">
  <ReportsLink />
</FeatureGate>;
```

A flag answers "does this org have the feature". A permission answers "may this person use it". Most gated features need
both. Details: [Organizations, feature flags](/docs/backend/organizations#feature-flags).

## Realtime

The backend pushes typed events over one WebSocket connection per client. Declare the event with `.emitter("paid")` in
`src/sockets/invoices/index.ts`, register that router in `src/sockets/index.ts`, and run `pnpm generate`. Then push from
`features/invoices/websockets/emitters.ts`, after the database write commits:

```ts
import { wsEmit } from "@backend/sockets/emit";

wsEmit.toUser({ userId, topic: "invoice/paid", data: invoice });
wsEmit.toOrganization({ organizationId, topic: "invoice/paid", data: invoice });
```

`toOrganization` reaches every connected member of that org. There are no per-team or per-project rooms, so send a
`teamId` or `projectId` in the payload and let the client filter, or collect the member ids and use `toUsers`.

Full walkthrough: [WebSockets](/docs/backend/websockets) and [frontend WebSockets](/docs/frontend/websockets).

## Scheduled jobs

Cron work runs in the backend process. Each fire runs once across all instances (a Postgres advisory lock guards it), so
two replicas never send the same reminder twice.

```ts
// apps/backend/src/jobs/invoice-overdue.job.ts
import type { JobDefinition } from "@backend/libs/scheduler/scheduler";

export const invoiceOverdueJob: JobDefinition = {
  name: "invoice-overdue",
  cron: "0 * * * *",
  run: async () => {
    await markOverdueInvoices();
  },
};
```

Add it to the `jobs` array in `apps/backend/src/jobs/index.ts`. Write jobs as sweepers: find the rows that are due and
handle them. The schedule of a business event (when an invoice is due) belongs in your tables, not in a cron pattern.

`catchUp: true` runs a missed fire once on boot. Leave it off for sweepers and turn it on when the fire time itself is
the event, like a nightly digest.

## Task queue

For work that is slow, calls a flaky API, or must retry, enqueue a task instead of doing it in the request. The queue
runs on Postgres (pg-boss), so there is no extra service to deploy.

```ts
// apps/backend/src/queues/invoice-pdf.queue.ts
import { z } from "zod";

import { defineQueue } from "@backend/libs/queue/queue";

export const invoicePdfQueue = defineQueue({
  name: "invoice-pdf",
  schema: z.object({ invoiceId: z.string() }),
  exclusive: true,
  run: async ({ data, signal }) => {
    await renderInvoicePdf(data.invoiceId, { signal });
  },
});
```

```ts
// inside the module that creates the invoice
await enqueue(invoicePdfQueue, { invoiceId }, { trx, key: invoiceId });
```

Register the queue in `apps/backend/src/queues/index.ts`. Passing `trx` means the task only exists if the transaction
commits. `exclusive` plus `key` makes a double click a no-op. Handlers can run twice after a crash, so make them safe to
repeat. Defaults: 3 retries with backoff, 15 minute timeout. Needs template v1.7.0 or newer.

## File storage

S3-compatible storage (RustFS locally) through three helpers in `@backend/libs/storage/storage`:

```ts
await uploadFile({ bucket: "invoice-attachments", key, body, contentType });
const url = await getFileUrl({ bucket: "invoice-attachments", key });
await deleteFile({ bucket: "invoice-attachments", key });
```

Buckets are created on first upload. Put the organization id in the key (`${organizationId}/${invoiceId}.pdf`) so files
stay grouped by tenant. Upload middleware and env: [Storage](/docs/backend/storage).

## Email

Resend plus React Email templates that live next to the feature that sends them. Call `resend.emails.send(...)` from the
module. Send after the database write commits, and from a queue task when the send is not the point of the request.
Details: [Mailing](/docs/backend/mailing).

## Cache

Valkey (Redis compatible) is on `valkey` from `@backend/context`. Use it for short-lived shared state and counters.
Never store the only copy of anything there: when Valkey is down, reads should fall back to Postgres or skip, not fail
the request.

## App shell slots

Your feature appears in the app by adding an entry to a constants file. The layout reads those files, so a feature never
edits the layout itself.

| Where it shows                       | File in your app                                     | Pack field        |
| ------------------------------------ | ---------------------------------------------------- | ----------------- |
| Sidebar or top nav                   | `frontend/src/constants/nav.constants.ts`            | `nav`             |
| Icon in the navbar                   | `frontend/src/constants/navbar-actions.tsx`          | `navbarActions`   |
| Avatar dropdown item                 | `frontend/src/constants/navbar-actions.tsx`          | `userMenuActions` |
| Block above sidebar navigation       | `frontend/src/constants/navbar-actions.tsx`          | `sidebarActions`  |
| Tab on the settings page             | `frontend/src/constants/settings-tabs.constants.tsx` | `settingsTabs`    |
| Step in organization onboarding      | `setup-organization/steps/index.ts`                  | `onboardingSteps` |
| Provider mounted at the app root     | `components/providers/providers.tsx`                 | `providers`       |
| Mobile drawer row                    | mobile `constants/navbar-actions.tsx`                | `drawerActions`   |
| Admin dashboard row                  | admin `dashboard.rows.tsx`                           | `dashboardRows`   |
| Admin organization tab, card, or row | admin `organization-details.*.tsx`                   | `orgDetails*`     |

A nav entry can carry a `permission: { action, subject }`. The nav filters it out for anyone without that permission,
which is how Billing stays hidden from non-admins. Hiding a link is only cosmetic, so the route still needs `hasAccess`.

## Shared enums

Anything both the backend and a client need (permission subjects, statuses, flag keys) goes in the shared enums package
and gets imported from both sides. That keeps a renamed status from compiling on one side and breaking on the other. See
[End-to-end typesafety](/docs/development/e2e-typesafety).

## Building it as a pack

Everything above is plain app code. To ship the same feature as an installable pack, the registry edits become manifest
fields and the CLI applies them on install. The field list and what each one patches:
[Manifest reference](/docs/packs-templates/pack-manifest).

## Related

- [Access control](/docs/backend/access-control)
- [Organize features](/docs/backend/organize-features)
- [Build your own pack](/docs/packs-templates/build-your-own)
