Hype StackHypeStack

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

MechanismUse it forIn your codePack manifest field
Request contextWho is calling, which org, what they may doctx.var.user, ctx.var.organizationId, ctx.var.abilitycontext
RoutesAn authenticated API under a prefixgetAuthenticatedApp() in routes/<domain>/index.tsroutes
PermissionsWho may read, write, or manage a thinghasAccess({ action, subject }), useUser().hasPermissionpermissions
Feature flagsTurning a feature on per organizationwithFeature(key), isFeatureEnabled, useFeatureFlag(key)none, edit catalog
RealtimePushing an update to a user or a whole orgwsEmit.toUser, wsEmit.toOrganizationsockets
Scheduled jobsWork on a clock: reminders, sweeps, digestsa JobDefinition in src/jobs/index.tsjobs
Task queueSlow or retryable work after a request returnsdefineQueue, enqueue(queue, data, { trx })queues
File storageUploads: avatars, logos, attachmentsuploadFile, getFileUrl, deleteFilenone
EmailTransactional mailresend.emails.send(...)none
CacheShort-lived shared state, rate limitsvalkey from @backend/contextnone
App shell slotsShowing up in nav, menus, settings, adminentries in the constants/* filesnav, *Actions
Shared enumsTypes and constants both sides import@<your-app>/enumsenums

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:

VariableWhat it is
userThe signed-in user
organizationIdThe active organization. Treat it as the tenant id for every query
abilityA CASL ability built from the member's role. See Permissions
sessionIdThe 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.

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.

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.

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 and 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.

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.

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 showsFile in your appPack field
Sidebar or top navfrontend/src/constants/nav.constants.tsnav
Icon in the navbarfrontend/src/constants/navbar-actions.tsxnavbarActions
Avatar dropdown itemfrontend/src/constants/navbar-actions.tsxuserMenuActions
Block above sidebar navigationfrontend/src/constants/navbar-actions.tsxsidebarActions
Tab on the settings pagefrontend/src/constants/settings-tabs.constants.tsxsettingsTabs
Step in organization onboardingsetup-organization/steps/index.tsonboardingSteps
Provider mounted at the app rootcomponents/providers/providers.tsxproviders
Mobile drawer rowmobile constants/navbar-actions.tsxdrawerActions
Admin dashboard rowadmin dashboard.rows.tsxdashboardRows
Admin organization tab, card, or rowadmin organization-details.*.tsxorgDetails*

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.

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.