Build apps
App structure
How to split an app built on Hane into screens, a BFF, the SDK, and a definitions directory, and what lives in Hane versus in your app.
This page describes the basic shape of a business app built on Hane. Like the reference app (vehicle lease management), it splits into screens, a BFF, and definitions, and keeps business data and business rules in Hane.
Overall shape
Section titled “Overall shape”Browser (screens) │ calls only /bff/*. Cookie session ▼BFF (your app's server) │ calls the public API with @hane/sdk, using the user's access token ▼Hane public API (gateway) │ ├─ business data, Workflows, Tasks, agents, documents, audit └─ definitions (hanectl apply → ReleaseBundle)- Screens call only the BFF. They never call Hane directly and hold neither API keys nor access tokens.
- The BFF owns user login and sessions, exchanges tokens with Hane, and calls the public API with the resulting access token.
- Definitions are manifests kept in Git and loaded into Hane with hanectl. Business rules (calculations, processes, assignments, agents) are written here.
Example layout
Section titled “Example layout”The reference app is laid out as follows. If you start your app with the same layout, you can copy the BFF code and the way definitions are organized as is.
Directorymy-app/
Directoryapp/ screens (React and Vite, for example)
Directorysrc/
- api.ts calls the BFF
- routes.tsx
Directorybff/ your app’s server
- app.ts routes (Hono, for example). Independent of where it runs
- server.ts local Node.js entry point and login
- worker.ts Cloudflare Workers entry point
Directorydefinitions/ manifests
- bundle.yaml what goes into the ReleaseBundle
Directoryschemas/
- …
Directoryrules/
- …
Directoryworkflows/
- …
Directoryagents/
- …
Directorypolicies/
- …
- package.json
Keep the BFF routes (app.ts) independent of where they run, and swap only login and session handling in server.ts or worker.ts. For example, locally you use PKCE against the development IdP with in-memory sessions; on Cloudflare Workers you use Cloudflare Access tokens.
Exchange tokens in the BFF
Section titled “Exchange tokens in the BFF”The BFF passes the user’s id_token from your IdP, together with your app’s service account API key, to Hane and exchanges them for an access token. This records the user as the principal and the app as the actor, so the person who decided a Task or confirmed a value is recorded as the user.
import { createHaneClient, unwrap } from "@hane/sdk";
export async function exchange(idToken: string) { const anon = createHaneClient({ baseUrl: process.env.HANE_API!, token: () => null }); return unwrap( anon.POST("/v1/auth/token", { body: { grant_type: "urn:ietf:params:oauth:grant-type:token-exchange", subject_token: idToken, subject_token_type: "urn:ietf:params:oauth:token-type:id_token", actor_token: process.env.HANE_API_KEY!, actor_token_type: "urn:hane:params:token-type:api_key", organization: "acme", project: "fleet", environment: "dev", }, }), );}The access token expires after 15 minutes. When it is about to expire, exchange the same id_token again; if the id_token is also about to expire, have the user log in again. For each request, create a client with that session’s access token.
const hane = (session: { accessToken: string }) => createHaneClient({ baseUrl: process.env.HANE_API!, token: () => session.accessToken });See Authentication for details.
What lives in Hane and what lives in your app
Section titled “What lives in Hane and what lives in your app”| In Hane | In your app |
|---|---|
| The source of truth for business data (records, value states, sources, relations) | Screens and routing |
| Rules, Workflows, schedules, assignment rules, agent definitions | Mapping your ledger’s columns to Hane fields (pre-processing for imports) |
| Tasks, approvals, records of external actions, audit | Login and sessions (integration with your IdP) |
| Conversations, AgentRuns, documents and search indexes | UI text and visual theme |
Rules to follow
Section titled “Rules to follow”- Only the BFF holds API keys and access tokens; never pass them to the browser. Use an httpOnly cookie session between the browser and the BFF.
- Take field labels from the type definition’s
title(GET /v1/data/types). Don’t duplicate them in your app’s dictionary. - When the BFF returns Hane errors to the screen with the code and Problem intact, screens can point to the field that was wrong.
- The BFF adds the CSP to responses for screens. When you load something from another origin, add it to the CSP too.