Skip to content
Open Bkper

App Architecture

Bkper platform apps use a single Worker with a client folder for the UI, /api/* routes with an OpenAPI contract, and /events handlers. Add shared code only when your app actually needs it.

Bkper platform apps use one Worker bundle per app and environment. The same Worker serves the browser client, app-defined /api/* routes, and Bkper event ingress at /events.

Treat /api/* as the reusable surface for app behavior. The bundled web client is one consumer of that API; scripts, external clients, and agents can call the same routes with bearer auth.

Structure

my-app/
├── client/
│ ├── index.html
│ └── src/ — Frontend UI (Vite + Lit)
├── server/
│ └── src/
│ ├── index.ts
│ └── handlers/
├── bkper.yaml
├── package.json
├── tsconfig.json
└── vite.config.ts

The default template is intentionally not a monorepo. Add a shared package only when the app actually needs one.

Client

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

  • Built with Vite — configured in the project’s vite.config.ts for fast builds and HMR during development
  • Built assets are deployed with the same Worker
  • Communicates with Bkper via bkper-js

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

Client authentication

The client authenticates users via the @bkper/web-auth SDK. OAuth is pre-configured on the platform — no client IDs, redirect URIs, or consent screens to set up.

import { Bkper } from 'bkper-js';
import { BkperAuth } from '@bkper/web-auth';
const auth = new BkperAuth({
baseUrl: isLocalDev ? window.location.origin : undefined,
onLoginSuccess: () => initializeApp(),
onLoginRequired: () => showLoginButton(),
});
await auth.init();
const bkper = new Bkper({
oauthTokenProvider: async () => auth.getAccessToken(),
});

This is the canonical browser pattern. Do not implement custom OAuth flows, redirect handling, or token refresh — the SDK and platform handle everything. See the @bkper/web-auth API Reference for the full SDK documentation.

Server Worker

The server folder 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 under /api/*
  • Bkper event ingress under /events
  • Type-safe access to platform services (KV, secrets) via c.env
import { Hono } from 'hono';
import { Bkper, Book } from 'bkper-js';
import type { Env } from '../../env.js';
const app = new Hono<{ Bindings: Env }>();
app.get('/api/v1/books', async c => {
const bkper = new Bkper();
const books = await bkper.getBooks();
return c.json({ books });
});
app.post('/events', async c => {
const event: bkper.Event = await c.req.json();
const bkper = new Bkper();
const book = new Book(event.book, bkper.getConfig());
// route by event.type
return c.json({ result: false });
});
app.get('*', c => c.env.ASSETS.fetch(c.req.raw));
export default app;

App API contract

Expose reusable app behavior through /api/* routes when it may be called by more than one client.

Use this shape:

  • Routes — Thin Hono handlers under /api/*.
  • Schemas — Typed request and response schemas for every route.
  • Services — Business behavior in server-side service modules.
  • OpenAPI — A machine-readable app API spec exposed at /openapi.json.

The default template starts public routes under /api/v1/* and generates typed client code from the same OpenAPI contract used by the shipped web client. Keep that contract current so scripts, external clients, and agents can connect without reverse-engineering the UI.

App API endpoints use these URLs:

Production: https://{appId}.bkper.app/api/*
Preview: https://{appId}-preview.bkper.app/api/*
Local: http://localhost:8787/api/*

The app OpenAPI spec lives at:

Production: https://{appId}.bkper.app/openapi.json
Preview: https://{appId}-preview.bkper.app/openapi.json
Local: http://localhost:8787/openapi.json

Example script call:

Terminal window
TOKEN="$(bkper auth token)"
curl \
-H "Authorization: Bearer ${TOKEN}" \
"https://my-app.bkper.app/api/v1/books"

Replace my-app with the app id from bkper.yaml.

Server API authentication

For deployed apps, server API routes under /api/* require a standard bearer token on the incoming request:

const token = auth.getAccessToken();
if (!token) throw new Error('Not authenticated');
const response = await fetch('/api/v1/data', {
headers: { Authorization: `Bearer ${token}` },
});

Dispatch validates the bearer token before your Worker runs. It then strips the Authorization header and passes only an internal outbound context, so app code should not read user tokens from request headers.

When the server route calls Bkper, use bkper-js without a token provider:

import { Bkper } from 'bkper-js';
app.get('/api/v1/books', async c => {
const bkper = new Bkper();
const books = await bkper.getBooks();
return c.json({
books: books.map(book => ({
id: book.getId(),
name: book.getName(),
})),
});
});

Platform outbound auth injects the validated user’s OAuth token on exact Bkper API requests. Browser sessions only allow access to app web pages; they do not authorize /api/* server routes or create outbound auth context.

Event handler authentication

On the Bkper Platform, /events is an internal Bkper delivery channel on the same Worker. App code must not read bkper-oauth-token, bkper-agent-id, or Authorization headers.

Use the same server-side Bkper API pattern as /api/* routes:

const bkper = new Bkper();
const book = new Book(event.book, bkper.getConfig());

Dispatch consumes the Core-sent event access token and strips platform headers before invoking your Worker. Platform outbound auth injects the token and app agent identity when your event handler calls the Bkper API.

For self-hosted event handlers, you receive and process event auth headers directly because the platform outbound layer is not involved.

Event routing pattern

A typical server routes events by type and delegates to small handlers:

app.post('/events', async c => {
const event: bkper.Event = await c.req.json();
if (!event.book) {
return c.json({ error: 'Missing book in event payload' }, 400);
}
const bkper = new Bkper();
const book = new Book(event.book, bkper.getConfig());
switch (event.type) {
case 'TRANSACTION_CHECKED':
return c.json(await handleTransactionChecked(book, event));
default:
return c.json({ result: false });
}
});

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

See Event Handlers for patterns and details.

When you don’t need every part

The platform can host different shapes, but the default template starts as a full app because /api/* routes give the app a reusable contract as it grows.

Use these shapes intentionally:

  • Full app — Client UI, /api/* backend logic, and /events automation in one Worker. This is the default growth path.
  • Event-only app — Keep server/ and omit deployment.client. Automates reactions to book events without a user interface.
  • UI-only app — Use client/ and keep a minimal server Worker for static assets only when the behavior is truly local to the browser. Add /api/* as soon as the behavior should be reusable by scripts, external clients, or agents.

Simple App Patterns

These are the minimal, canonical patterns for common app tasks. Use them as starting points and resist adding complexity unless the user explicitly asks for it.

Client-only UI with authentication

The smallest useful app can keep browser-only display logic in client/. No custom server routes, no event handlers, no custom auth logic.

If the behavior should be reused by scripts, external clients, or agents, expose it through /api/* instead of keeping it only in the UI.

client/src/app.ts
import { Bkper } from 'bkper-js';
import { BkperAuth } from '@bkper/web-auth';
const auth = new BkperAuth({
baseUrl: window.location.origin.includes('localhost') ? undefined : window.location.origin,
onLoginSuccess: () => render(),
onLoginRequired: () => renderLogin(),
});
await auth.init();
const bkper = new Bkper({
oauthTokenProvider: async () => auth.getAccessToken(),
});
async function render() {
const books = await bkper.getBooks();
// render books
}

Key points:

  • BkperAuth handles OAuth, token refresh, and session management internally.
  • auth.getAccessToken() returns a valid token synchronously after init() resolves.
  • Do not add server-side /auth/* routes. Do not implement refresh_token logic yourself.

Fetch and display data

const book = await bkper.getBook(bookId);
const accounts = await book.getAccounts();
// render accounts

Use bkper-js for all API calls. Do not call the REST API directly when bkper-js provides the same method.

Library Usage Reference

TaskUseDo not use
Client authentication@bkper/web-auth (BkperAuth, getAccessToken)Custom OAuth flows, manual fetch('/auth/refresh'), google-auth-library in the browser
API calls from clientbkper-js (Bkper, Book, Account, Transaction)Direct fetch() to REST endpoints
API calls from app server /api/* routeIncoming Authorization: Bearer <token> + server-side new Bkper()Reading OAuth tokens in server code, relying on browser sessions for API auth
API calls from platform event handlerServer-side new Bkper() in /eventsReading bkper-oauth-token or bkper-agent-id in platform app code
Local development servernpm run dev (template script)Manual miniflare + cloudflared invocations
Event handler routingswitch (event.type) in server/src/index.ts or server/src/handlers/Middleware frameworks, external webhook routers
UI components@bkper/web-design + LitHeavy UI frameworks unless the user explicitly requests them

Common Pitfalls

Avoid these patterns even if they seem necessary. The platform or SDK already solves the problem.

  1. Implementing custom OAuth on the server

    • @bkper/web-auth manages the full OAuth lifecycle on the client. The platform handles tokens. Adding a server-side auth layer is unnecessary and will break.
  2. Adding /api/auth/refresh or similar routes

    • Token refresh is internal to @bkper/web-auth. Exposing it via Hono routes creates security surface area and duplicates platform functionality.
  3. Relying on browser sessions for server API auth

    • Sessions let users open app web pages, but /api/* routes require Authorization: Bearer <token>. Dispatch validates bearer tokens and platform outbound uses that validated context for Bkper API calls.
  4. Modifying server/ for a simple UI task

    • If the user only asked for a client-side feature, do not touch server routes. The Vite dev server proxies /api to the Miniflare worker automatically. Add routes when the behavior should be reusable by the shipped client, scripts, external clients, or agents.
  5. Installing additional auth or HTTP libraries

    • bkper-js and @bkper/web-auth are the only packages you need for Bkper API access and authentication. Adding axios, google-auth-library, or similar is almost always wrong.
  6. Creating event handlers when the user asked for a UI-only feature

    • If the user says “show me a list of books in a popup,” that is a client-only task. Do not add /events logic or subscribe to webhooks.
  7. Calling REST endpoints directly when bkper-js has the method

    • If bkper-js exposes book.getTransactions(), use it. Do not fetch('https://api.bkper.com/...') and parse JSON manually.
  8. Reverse-engineering SDK internals

    • Use the public API surface documented in the API reference. Do not read SDK source to find private methods or internal request patterns.