Skip to content

App Architecture

Bkper platform apps use a three-package monorepo: a Lit + Vite client for the UI, a Hono server on Cloudflare Workers for the API, and an events package for webhook-driven automation. Mix and match only what you need.

Bkper platform apps follow a three-package monorepo pattern. Each package handles a distinct concern, all deployed to the same {appId}.bkper.app domain.

Structure

packages/
├── shared/ — Shared types and utilities
├── web/
│ ├── client/ — Frontend UI (Vite + Lit)
│ └── server/ — Backend API (Hono)
└── events/ — Event handler (webhooks)

The packages are connected via Bun workspaces. Import shared code from @my-app/shared in any package.

Web client

The client package builds a browser UI with Lit and @bkper/web-design for consistent Bkper styling. Authentication uses @bkper/web-auth.

  • Built with Vite — fast builds and HMR during development
  • Static assets served by the web server handler
  • Communicates with Bkper via bkper-js (authenticated with the web-auth SDK)

This is where your app’s UI lives — book pickers, account lists, reports, forms.

Web server

The server package runs on Cloudflare Workers using Hono as the web framework. It handles:

  • Serving the client’s static assets
  • Custom API routes for your app’s backend logic
  • Type-safe access to platform services (KV, secrets) via c.env
import { Hono } from 'hono';
import type { Env } from '../../../../env.js';
const app = new Hono<{ Bindings: Env }>();
app.get('/api/data', async (c) => {
const cached = await c.env.KV.get('my-key');
return c.json({ data: cached });
});
export default app;

Events handler

The events package receives webhook calls from Bkper when subscribed events occur. It’s a separate Hono app that processes events and returns responses.

import { Hono } from 'hono';
import type { Env } from '../../../env.js';
const app = new Hono<{ Bindings: Env }>().basePath('/events');
app.post('/', async (c) => {
const event: bkper.Event = await c.req.json();
switch (event.type) {
case 'TRANSACTION_CHECKED':
return c.json(await handleTransactionChecked(book, event));
default:
return c.json({ result: false });
}
});
export default app;

Event handlers run at https://{appId}.bkper.app/events in production. During development, a Cloudflare tunnel routes events to your local machine.

See Event Handlers for patterns and details.

Shared package

Common types, utilities, and constants used across packages:

packages/shared/src/types.ts
export interface EventResult {
result?: string | string[] | boolean;
error?: string;
warning?: string;
}
// packages/shared/src/constants.ts
export const APP_NAME = 'my-app';

Import in any package:

import type { EventResult } from '@my-app/shared';

Note: The Env type (KV bindings, secrets) lives in the root env.d.ts file, auto-generated from bkper.yaml. Import it as import type { Env } from '../../../env.js' — it is not part of the shared package.

When you don’t need all three

Not every app needs a UI, API, and event handler:

  • Event-only app — Just the events package. Automates reactions to book events without a user interface. Remove the web section from bkper.yaml.
  • UI-only app — Just the web packages. Opens via a context menu to display data or collect input. Remove the events section from bkper.yaml.
  • Full app — All three packages. Interactive UI with backend logic and event-driven automation.

The template includes all three by default. Remove what you don’t need.