# Apps (Full)

---
source: /docs/platform/apps/ai.md

# Add Bkper AI to an App

Bkper AI is the preferred inference provider for Bkper Platform apps. It uses the authenticated user's included AI allowance, attributes usage to the app, and does not require the app to store provider credentials.

Use another provider only when Bkper AI lacks a required capability, model, compliance boundary, or customer-mandated provider. External providers require their own authentication, secrets, billing, and privacy review.

This guide shows a non-streaming language response with strict structured output. For bounded boolean, choice, or score judgments, use the typed evaluation endpoint instead. Streaming, tool calls, file inputs, and agent runtimes require additional design.

## Request flow

Keep model calls behind the app's typed `/api/*` contract:

1. The web client calls an app `/api/*` route with a Bkper bearer token. The template's `auth.authenticatedFetch()` handles this.
2. Bkper validates the token, mounts the user and app identity as outbound context, and removes the token before invoking the app Worker.
3. The Worker calls `https://ai.bkper.app/v1/*` without reading, storing, or forwarding the token.
4. Platform outbound injects Bkper authorization and overwrites `bkper-agent-id` and `bkper-ai-source` with the authenticated app identity.

Event handlers use the same Worker-to-Bkper-AI step. Their outbound context comes from the authenticated Bkper event. A normal page request does not establish user outbound context, so start interactive inference from an authenticated `/api/*` route rather than a page handler.

## Call the app API from the client

Use the authenticated fetch provider already configured by the app template:

```ts
interface AnalyzeRequest {
    first: {
        date: string;
        amount: string;
        description: string;
        fromAccount: string | null;
        toAccount: string | null;
    };
    second: {
        date: string;
        amount: string;
        description: string;
        fromAccount: string | null;
        toAccount: string | null;
    };
}

export async function analyzePair(auth: AuthProvider, request: AnalyzeRequest): Promise {
    return auth.authenticatedFetch('/api/v1/analyze', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(request),
    });
}
```

In a full app, define this operation in the server's Zod/OpenAPI schemas and call it through the generated typed client. The important boundary is that the client authenticates the app API request; the Worker never handles that bearer token directly.

## Discover the current default model

The live model catalog is authoritative. It publishes defaults by model type, model IDs, modalities, capabilities, limits, and effective usage rates. Language models use `POST /v1/responses`; evaluation models use `POST /v1/evaluations`.

```ts
const AI_BASE_URL = 'https://ai.bkper.app/v1';
type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise;

function isRecord(value: unknown): value is Record<string, unknown> {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
}

export async function getStructuredOutputModel(fetcher: Fetcher = fetch): Promise<string> {
    const response = await fetcher(`${AI_BASE_URL}/models`);
    if (!response.ok) {
        throw new Error(`Bkper AI model discovery failed (${response.status}).`);
    }

    const catalog: unknown = await response.json();
    if (
        !isRecord(catalog) ||
        typeof catalog.default_model !== 'string' ||
        !Array.isArray(catalog.data)
    ) {
        throw new Error('Bkper AI returned an invalid model catalog.');
    }

    const defaultModel = catalog.default_model;
    const model = catalog.data.find(item => isRecord(item) && item.id === defaultModel);
    if (
        !isRecord(model) ||
        !isRecord(model.structured_output) ||
        model.structured_output.json_schema !== true ||
        model.structured_output.strict !== true
    ) {
        throw new Error('The default Bkper AI model does not support strict structured output.');
    }
    return defaultModel;
}
```

This example reads `default_model` to select the current default language model, then validates the capability required by the request. If an app requires another modality, file type, reasoning level, or limit, intentionally select and validate another language model from the catalog's `data` array. Apps may cache the catalog briefly rather than fetching it for every inference request.

## Choose the endpoint by output shape

| Application need                                                   | Model type   | Endpoint               |
| ------------------------------------------------------------------ | ------------ | ---------------------- |
| Boolean probability, one bounded choice, or an ordered score       | `evaluation` | `POST /v1/evaluations` |
| Generated text, a custom JSON object, tool calls, images, or files | `language`   | `POST /v1/responses`   |

Jev can evaluate several independent questions against one shared state in parallel. Keep thresholds, deterministic rules, and resulting Book actions in application code. Bkper CLI Agent does not use Jev; typed evaluations are a separate API capability for apps and compatible clients.

See [Bkper AI Provider → Typed evaluations](https://bkper.com/docs/ai/bkper-ai-provider.md#typed-evaluations) for the complete request pattern.

## Request strict structured output

Keep inference in a server service and make `fetch` injectable for unit tests. This example sends only the transaction facts needed for duplicate evaluation. It omits transaction IDs, Account IDs, unrelated properties, and other Book data.

```ts
const EvaluationJsonSchema = {
    type: 'object',
    properties: {
        duplicate: { type: 'boolean' },
        strength: { type: 'string', enum: ['Strong', 'Possible'] },
        explanation: { type: 'string', maxLength: 180 },
    },
    required: ['duplicate', 'strength', 'explanation'],
    additionalProperties: false,
} as const;

export interface DuplicateEvaluation {
    duplicate: boolean;
    strength: 'Strong' | 'Possible';
    explanation: string;
}

export class BkperAiError extends Error {
    constructor(
        readonly status: number,
        readonly code: string,
        message: string
    ) {
        super(message);
        this.name = 'BkperAiError';
    }
}

export async function evaluateDuplicate(
    candidate: AnalyzeRequest,
    fetcher: Fetcher = fetch
): Promise {
    const model = await getStructuredOutputModel(fetcher);
    const response = await fetcher(`${AI_BASE_URL}/responses`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
            model,
            instructions:
                'Decide whether both records represent the same movement. ' +
                'Return Strong only when the evidence is compelling.',
            input: [
                {
                    role: 'user',
                    content: [
                        {
                            type: 'input_text',
                            text: JSON.stringify(candidate),
                        },
                    ],
                },
            ],
            text: {
                format: {
                    type: 'json_schema',
                    name: 'duplicate_evaluation',
                    schema: EvaluationJsonSchema,
                    strict: true,
                },
            },
            stream: false,
            store: false,
        }),
    });

    const payload: unknown = await response.json();
    if (!response.ok) {
        const error = readAiError(payload);
        throw new BkperAiError(response.status, error.code, error.message);
    }

    const value: unknown = JSON.parse(getOutputText(payload));
    if (!isDuplicateEvaluation(value)) {
        throw new Error('Bkper AI output did not match the required schema.');
    }
    return value;
}
```

Do not add an `Authorization`, `bkper-agent-id`, or `bkper-ai-source` header to the Worker's Bkper AI request. Platform outbound derives those values from the authenticated app request or event and overwrites them before dispatch.

## Validate the response and preserve errors

Strict structured output constrains generation, but the app must still parse and validate the returned value before using it.

```ts
function getOutputText(payload: unknown): string {
    if (!isRecord(payload) || payload.status !== 'completed' || !Array.isArray(payload.output)) {
        throw new Error('Bkper AI did not return a complete response.');
    }

    const texts: string[] = [];
    for (const item of payload.output) {
        if (!isRecord(item) || item.type !== 'message' || !Array.isArray(item.content)) continue;
        for (const part of item.content) {
            if (isRecord(part) && part.type === 'output_text' && typeof part.text === 'string') {
                texts.push(part.text);
            }
        }
    }
    if (texts.length === 0) throw new Error('Bkper AI returned no output text.');
    return texts.join('');
}

function isDuplicateEvaluation(value: unknown): value is DuplicateEvaluation {
    return (
        isRecord(value) &&
        typeof value.duplicate === 'boolean' &&
        (value.strength === 'Strong' || value.strength === 'Possible') &&
        typeof value.explanation === 'string' &&
        value.explanation.length <= 180
    );
}

function readAiError(payload: unknown): { code: string; message: string } {
    if (
        isRecord(payload) &&
        isRecord(payload.error) &&
        typeof payload.error.code === 'string' &&
        typeof payload.error.message === 'string'
    ) {
        return { code: payload.error.code, message: payload.error.message };
    }
    return { code: 'bkper_ai_error', message: 'Bkper AI request failed.' };
}
```

Preserve the upstream HTTP status, error code, and message when mapping a `BkperAiError` into the app's typed error envelope. Bkper AI centralizes actionable messages such as allowance guidance and pricing links. Bot event responses may reuse that message directly when the surface supports it. Interactive apps can use the status and code to provide a tailored experience without duplicating the upstream policy or CTA.

## Test the boundary

Use a mocked `fetch` to protect the integration contract without making live model calls:

```ts
expect(capturedRequest.headers.get('authorization')).toBeNull();
expect(requestBody.store).toBe(false);
expect(requestBody.stream).toBe(false);
expect(requestBody.text).toMatchObject({
    format: { type: 'json_schema', strict: true },
});
```

Also test that the service:

- validates that the catalog's `default_model` supports strict structured output;
- rejects malformed or schema-incompatible output;
- preserves Bkper AI error status, code, and message;
- does not send internal identifiers or unrelated Book data.

## Implementation checklist

Before considering the integration complete:

- [ ] The client calls a typed `/api/*` route through authenticated fetch.
- [ ] Worker code never reads, stores, or forwards the Bkper bearer token.
- [ ] The app discovers models from `GET /v1/models`, checks the model `type`, and uses the matching endpoint.
- [ ] Inference runs in a server service with an injectable `fetch`.
- [ ] The request uses strict structured output, `stream: false`, and `store: false`.
- [ ] Only data required for the task is sent to inference.
- [ ] Returned JSON is parsed and independently validated.
- [ ] Error status, code, and message remain available to the caller.
- [ ] Unit tests cover the request, response, validation, and error boundaries.
- [ ] The app's normal `npm run check` or `bun run check` succeeds.

## Next steps

- [Read the client-agnostic Bkper AI Provider guide](https://bkper.com/docs/ai/bkper-ai-provider.md) for privacy boundaries, the complete supported profile, and advanced features.
- [Inspect the live model catalog](https://ai.bkper.app/v1/models).
- [Browse the generated AI API reference](https://bkper.com/docs/api/ai.md) when exact request or response schema details are needed.
- [Review the Merge Duplicates implementation](https://github.com/bkper/bkper-apps/tree/main/merge-duplicates) for a platform-app example with deterministic candidate filtering, strict structured output, and human-confirmed merges.

---
source: /docs/platform/apps/app-listing.md

# App Listing

All Bkper apps are listed on the Automations Portal at _[app.bkper.com](https://app.bkper.com/) > Automations > Apps_. Each app has its own page with logo, description, and details:

![App listing on the Automations Portal](https://bkper.com/docs/_astro/bkper-app-listing.BgcbAsjE.png)

App listings are populated from the fields you declare in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md). Sync metadata changes with `bkper app sync`. Deploying code is a separate step.

## Listing fields

Make sure your `bkper.yaml` has the following fields populated for a complete listing:

```yaml
id: your-app-id
name: Your App Name
description: A clear description of what your app does

logoUrl: https://your-app.bkper.app/images/logo.svg
logoUrlDark: https://your-app.bkper.app/images/logo-dark.svg

ownerName: Your Name or Organization
ownerWebsite: https://yourwebsite.com

website: https://your-app.bkper.app
```

See [App Configuration](https://bkper.com/docs/platform/apps/configuration.md) for the full `bkper.yaml` reference.

## Default visibility

By default, installation is limited to the users you've declared in `bkper.yaml`:

```yaml
# Specific Bkper usernames
users: alice bob

# Your entire domain
users: *@yourcompany.com
```

Use Bkper usernames for individual access, not email addresses.

Your team can install and use the app, but it doesn't appear in the public Bkper app directory for other users.

## Publishing to all users

To make your app available to all Bkper users, contact us at [support@bkper.com](mailto:support@bkper.com?subject=Publish+Bkper+App). We'll review your app and, once approved, publish it.

### What the review involves

- **Functionality check** — The app works correctly and handles errors gracefully
- **Quality review** — The implementation follows the [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md)
- **Security review** — Event handlers are idempotent and include loop prevention
- **Listing quality** — The app has a clear name, description, logo, and user-facing documentation

### README matters

Your app's `README.md` is displayed to end users on the app listing page. Write it for the people who will install and use your app — not for developers.

**README should explain:**

- What the app does from a user's perspective
- How to use it (step-by-step for non-technical users)
- What features are available
- API access details when the app intentionally exposes `/api/*` routes for users or integrators

**API access details should stay concise:**

- App base URL for production and preview
- OpenAPI spec URL at `/openapi.json`
- One minimal authenticated example, such as a `curl` call with `Authorization: Bearer <token>`

**README should NOT contain:**

- Tech stack or architecture details
- Build commands or development setup
- Project structure or internal file paths
- Long API references, generated schemas, SDK internals, or route-by-route developer docs

Put developer documentation in `AGENTS.md` or internal docs instead. Keep `README.md` focused on the user experience and any integration entry points users need.

### Where published apps appear

Once published, your app appears in:

- **[bkper.com/apps](https://bkper.com/apps)** — The public app directory
- **Automations Portal** — Inside every Bkper book, users can find and install your app

---
source: /docs/platform/apps/architecture.md

# App Architecture

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; scripts, external clients, and agents can call the same routes with bearer authentication.

Follow the [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md) when implementing, changing, or reviewing an app.

## Structure

```txt
my-app/
├── client/
│   ├── index.html
│   ├── package.json
│   ├── vite.config.ts
│   └── src/
│       ├── api/
│       ├── auth/
│       ├── components/
│       └── services/
├── server/
│   ├── package.json
│   └── src/
│       ├── api/
│       ├── events/
│       ├── services/
│       └── index.ts
├── scripts/
├── bkper.yaml
├── env.d.ts
├── package.json
├── package-lock.json
└── tsconfig.json
```

The root npm workspace orchestrates development, tests, builds, and deployment. The template keeps browser dependencies in `client/` and Worker dependencies in `server/`. Add a shared package only when both sides actually need one.

## Client

The client uses:

- [Lit](https://lit.dev/) for components and rendering.
- [Web Awesome](https://webawesome.com/) for UI components.
- [`@bkper/web-design`](https://www.npmjs.com/package/@bkper/web-design) for Bkper design tokens.
- [Vite](https://vitejs.dev/) for development and production builds, configured in `client/vite.config.ts`.

Client code has two data paths. Choose based on who owns the behavior:

- **Direct Bkper calls** use `bkper-js` for generic Bkper data needed only by the browser UI.
- **App API calls** use the generated typed client in `client/src/api/` with `auth.authenticatedFetch()` for app-owned behavior, especially when it needs server-only capabilities or more than one caller.

Keep app-owned behavior in one place. Do not implement the same behavior separately in the UI and the app API.

For stateful feature components, co-locate view, controller, and CSS files in one folder under `components/`. Simple presentational components can remain in one file.

### Client authentication

The client authenticates users with [`@bkper/web-auth`](https://www.npmjs.com/package/@bkper/web-auth). OAuth is preconfigured on the platform, so there are no client IDs, redirect URIs, or consent screens to configure.

```ts
import { Bkper } from 'bkper-js';
import { BkperAuth } from '@bkper/web-auth';

const isLocalDev = ['localhost', '127.0.0.1'].includes(window.location.hostname);
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(),
});
```

`@bkper/web-auth` handles login, redirects, and token refresh. The template keeps this behavior behind `client/src/auth/auth-session.ts`.

See the [@bkper/web-auth API Reference](https://bkper.com/docs/api/bkper-web-auth.md) for the full SDK documentation.

## Server Worker

The server runs on [Cloudflare Workers](https://developers.cloudflare.com/workers/) and uses [Hono](https://hono.dev/) with typed OpenAPI routes. It handles:

- app API routes under `/api/*`;
- Bkper event ingress under `/events`;
- platform services such as KV and secrets through `c.env`;
- static client assets through the `ASSETS` binding.

The Worker entry point composes those concerns while routes delegate business behavior to services:

```ts
import { OpenAPIHono } from '@hono/zod-openapi';
import { registerApiRoutes } from './api/routes.js';
import { registerEventRoutes } from './events/routes.js';
import { appContextMiddleware, type AppEnv } from './app-context.js';

const app = new OpenAPIHono();

app.use('/api/*', appContextMiddleware());
app.use('/events', appContextMiddleware());
registerApiRoutes(app);
registerEventRoutes(app);

app.get('*', c => c.env.ASSETS.fetch(c.req.raw));

export default app;
```

## App API contract

The default template publishes versioned routes under `/api/v1/*` and exposes their OpenAPI contract at `/openapi.json`.

| Concern                      | Location                              |
| ---------------------------- | ------------------------------------- |
| OpenAPI metadata             | `server/src/api/openapi.ts`           |
| Request and response schemas | `server/src/api/schemas.ts`           |
| Thin route handlers          | `server/src/api/routes.ts`            |
| Business behavior            | `server/src/services/`                |
| Generated client types       | `client/src/api/generated/types.d.ts` |
| Typed client wrapper         | `client/src/api/app-api.ts`           |
| Contract snapshot            | `server/test/api/openapi.snapshot.json` |

When changing the API:

1. Update schemas, services, routes, and focused unit tests.
2. Run `npm run api` to regenerate client types.
3. Review the OpenAPI snapshot when the public contract changes.
4. Run `npm run check` before release.

Keep existing `/api/v1/*` contracts backward compatible. Additive fields and routes can remain in `v1`; breaking changes belong in a new namespace such as `/api/v2/*`.

### Reuse Bkper API types

When an app API returns payloads from the Bkper REST API, reference the canonical types from `@bkper/bkper-api-types` instead of recreating their fields in the app. The template's balances endpoint demonstrates this with `bkper.Book`:

```ts
export const BookSchema = z
    .custom<bkper.Book>(value => value !== undefined)
    .openapi('Book', {
        type: 'object',
        additionalProperties: true,
        'x-typescript-type': 'bkper.Book',
    });
```

The template's API generator recognizes `x-typescript-type`, imports `@bkper/bkper-api-types`, and emits the canonical reference in `client/src/api/generated/types.d.ts`:

```ts
Book: bkper.Book;
```

Both the server and client packages include `@bkper/bkper-api-types` for local typechecking. Run `npm run api` after adding or changing these schemas.

This bridge provides compile-time types but does not validate payload fields at runtime. Use it directly for trusted Bkper-owned responses. Request bodies, especially those used to create or modify Book resources, still require concrete Zod validation.

### URLs

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

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

Example script call:

```bash
TOKEN="$(bkper auth token)"

curl \
  -H "Authorization: Bearer ${TOKEN}" \
  "https://my-app.bkper.app/api/v1/ping"
```

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

### Server API authentication

Deployed `/api/*` routes require a Bkper OAuth bearer token. The template client uses `authenticatedFetch()` so token attachment and refresh stay inside `@bkper/web-auth`:

```ts
const response = await auth.authenticatedFetch('/api/v1/ping');
```

Dispatch validates the incoming bearer token and strips the `Authorization` header before the Worker runs. Server code should not read or forward the token.

When a route calls Bkper, create the SDK without a token provider:

```ts
import { Bkper } from 'bkper-js';

const bkper = new Bkper();
const books = await bkper.getBooks();
```

Platform outbound authentication injects the validated user's OAuth token on Bkper API requests.

### Authorize app operations

Authentication identifies the Bkper user, but each app must authorize sensitive data and actions server-side. Client-side checks are not an authorization boundary.

See [App Security](https://bkper.com/docs/platform/apps/security.md) for domain restrictions, Book permissions, and app installation checks.

## Event handlers

Platform event deliveries reach `/events` on the same Worker. Event adapters live in `server/src/events/`, while reusable business behavior belongs in `server/src/services/`.

Event code uses server-side `new Bkper()` and must not read `bkper-oauth-token`, `bkper-agent-id`, or `Authorization` headers. Dispatch and platform outbound authentication handle the event token and app agent identity.

See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for routing, responses, loop prevention, and event types. Self-hosted handlers process event authentication directly because the platform outbound layer is not involved.

## App shapes

The platform supports different shapes:

- **Full app** — Client UI, `/api/*` backend behavior, and `/events` automation in one Worker. This is the default template.
- **Event-only app** — Keep `server/` and omit `deployment.client`.
- **UI-only app** — Keep a minimal Worker for static assets when behavior is truly browser-only. Add `/api/*` when scripts, integrations, or agents should reuse that behavior.

---
source: /docs/platform/apps/configuration.md

# App Configuration

The `bkper.yaml` file is the single configuration file for your Bkper app. It defines the app's identity, access control, menu integration, event handling, and deployment settings.

It lives in the root of your project. Use `bkper app sync` to push metadata changes to Bkper, and use `bkper app deploy` to upload built code to the platform.

## Minimal example

```yaml
id: my-app
name: My App
description: A Bkper app that does something useful
developers: myuser
```

## Starter example

From the [app template](https://github.com/bkper/bkper-app-template):

```yaml
id: my-app
name: My App
description: A Bkper app that does something useful

logoUrl: https://my-app.bkper.app/images/logo-light.svg
logoUrlDark: https://my-app.bkper.app/images/logo-dark.svg

website: https://my-app.bkper.app
ownerName: Bkper
ownerLogoUrl: https://avatars.githubusercontent.com/u/11943086?v=4
ownerWebsite: https://bkper.com

developers: someuser *@yoursite.com
users: someuser *@yoursite.com

menuUrl: https://my-app.bkper.app?bookId=${book.id}
menuUrlDev: https://my-app-preview.bkper.app?bookId=${book.id}
menuOpenMode: SIDEBAR

webhookUrl: https://my-app.bkper.app/events
webhookUrlDev: https://my-app-preview.bkper.app/events
apiVersion: v5
events:
    - TRANSACTION_CHECKED

deployment:
    server: server/src/index.ts
    client: client
    services:
        - KV
    secrets: []
    compatibility_date: '2026-01-28'
```

### App identity

| Field         | Description                                                                                               |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| `id`          | Permanent app identifier. Lowercase letters, numbers, and hyphens only. Cannot be changed after creation. |
| `name`        | Display name shown in the Bkper UI.                                                                       |
| `description` | Brief description of what the app does.                                                                   |

### Branding

| Field         | Description                                |
| ------------- | ------------------------------------------ |
| `logoUrl`     | App logo for light mode (SVG recommended). |
| `logoUrlDark` | App logo for dark mode.                    |
| `website`     | App website or documentation URL.          |

### Ownership

| Field          | Description                                                  |
| -------------- | ------------------------------------------------------------ |
| `ownerName`    | Developer or company name.                                   |
| `ownerLogoUrl` | Owner's logo/avatar URL.                                     |
| `ownerWebsite` | Owner's website.                                             |
| `repoUrl`      | Source code repository URL.                                  |
| `repoPrivate`  | Whether the repository is private.                           |
| `deprecated`   | Hides from app listings; existing installs continue working. |

### Access control

| Field        | Description                                                                                                                                      |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `developers` | Who can update the app and deploy new versions. Accepts comma- or space-separated Bkper usernames and domain wildcards such as `*@yoursite.com`. |
| `users`      | Who can install and use the app. Uses the same format as `developers`; leave empty for public apps.                                              |

### Menu integration

| Field          | Description                                                                 |
| -------------- | --------------------------------------------------------------------------- |
| `menuUrl`      | Production menu URL. Supports [variable substitution](#menu-url-variables). |
| `menuUrlDev`   | Development menu URL, typically a preview or local app URL.                 |
| `menuText`     | Custom menu text (defaults to app name).                                    |
| `menuOpenMode` | How the app menu opens: `SIDEBAR` (default), `EXPANDED`, or `NEW_TAB`.      |

`SIDEBAR` and `EXPANDED` Apps can receive live context updates while their iframe stays loaded. `NEW_TAB` Apps receive context only in the URL used to open the tab.

See [Context Menu](https://bkper.com/docs/platform/apps/context-menu.md#live-context-updates) for menu URL configuration and live context updates.

### Menu URL variables

The following variables can be used in `menuUrl` and `menuUrlDev`:

| Variable                    | Description                              |
| --------------------------- | ---------------------------------------- |
| `${book.id}`                | Current book ID                          |
| `${book.properties.xxx}`    | Book property value                      |
| `${account.id}`             | Selected account ID                      |
| `${account.name}`           | Selected account name                    |
| `${account.properties.xxx}` | Account property value                   |
| `${group.id}`               | Selected group ID                        |
| `${group.name}`             | Selected group name                      |
| `${group.properties.xxx}`   | Group property value                     |
| `${transactions.ids}`       | Comma-separated selected transaction IDs |
| `${transactions.query}`     | Current search query                     |

### Event handling

| Field           | Description                                                                         |
| --------------- | ----------------------------------------------------------------------------------- |
| `webhookUrl`    | Production webhook URL for receiving events.                                        |
| `webhookUrlDev` | Development webhook URL (auto-updated by `bkper app dev`).                          |
| `apiVersion`    | API version for event payloads (currently `v5`).                                    |
| `events`        | List of [event types](https://bkper.com/docs/platform/apps/event-handlers.md#event-types) to subscribe to. |

See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for details on handling events.

### File patterns

| Field          | Description                                                                                                            |
| -------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `filePatterns` | List of glob patterns (e.g., `*.ofx`, `*.csv`). When a matching file is uploaded, a `FILE_CREATED` event is triggered. |

### Properties schema

The `propertiesSchema` field defines autocomplete suggestions for custom properties in the Bkper UI, helping users discover the correct property keys and values for your app.

Suggested keys must follow the same custom property rules as user-entered keys, including the 30-character maximum after normalization.

```yaml
propertiesSchema:
    book:
        keys:
            - my_app_enabled
        values:
            - 'true'
            - 'false'
    group:
        keys:
            - my_app_category
    account:
        keys:
            - my_app_sync_id
    transaction:
        keys:
            - my_app_reference
```

### Deployment

For apps deployed to the [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md):

| Field                           | Description                                                                                                            |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `deployment.server`             | TypeScript entry point for the single server Worker. It serves `/api/*`, `/events`, and static assets.                 |
| `deployment.client`             | Optional Vite/static client root. Built assets are deployed with the same Worker.                                      |
| `deployment.services`           | Platform services to provision. Currently: `KV` (key-value storage).                                                   |
| `deployment.secrets`            | Secret names used by the app. Managed via `bkper app secrets`.                                                         |
| `deployment.compatibility_date` | [Cloudflare Workers compatibility date](https://developers.cloudflare.com/workers/configuration/compatibility-dates/). |

See [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) for the full deployment workflow.

---
source: /docs/platform/apps/context-menu.md

# Context Menu

Apps can add context menu items on the Transactions page **More** menu in your Books. This lets you open dynamically built URLs with reference to the current Book's context — the active query, selected account, date range, and more.

Embedded interfaces should follow the [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md) for visual consistency, startup behavior, and Book-context verification.

## How it works

Once you install an App with a menu configuration, a new menu item appears in your Book:

![Custom menu item in the More menu](https://bkper.com/docs/_astro/bkper-report-menu.eu_pyhWe.png)

When clicked, a popup opens carrying the particular context of that book at that moment:

![App menu popup with book context](https://bkper.com/docs/_astro/bkper-app-menu-popup.BQ95Y-ki.png)

## Configuration

Configure the menu URL in your [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md):

```yaml
menuUrl: https://my-app.bkper.app?bookId=${book.id}&query=${transactions.query}
```

When the user clicks the menu item, the URL expressions `${xxxx}` are replaced with contextual information from the Book:

```
https://my-app.bkper.app?bookId=abc123&query=account:Sales
```

Where `abc123` is the current Book id and `account:Sales` is the current query being executed.

### Development URL

Use `menuUrlDev` to keep developer testing separate from production. The app template points it to the preview deployment:

```yaml
menuUrl: https://my-app.bkper.app?bookId=${book.id}&query=${transactions.query}
menuUrlDev: https://my-app-preview.bkper.app?bookId=${book.id}&query=${transactions.query}
```

During local development, you can instead point it to the local Worker URL at `http://localhost:8787`. The development URL is used when an app developer clicks the menu item.

### Menu open mode

Control how the menu opens with `menuOpenMode`:

```yaml
menuOpenMode: SIDEBAR
```

| Mode       | Behavior                                                              |
| ---------- | --------------------------------------------------------------------- |
| `SIDEBAR`  | Opens in a narrow side panel (default).                               |
| `EXPANDED` | Opens in a wider panel with more room for complex UIs.                |
| `NEW_TAB`  | Opens the menu URL in a new browser tab instead of an embedded panel. |

### Live context updates

Bkper keeps embedded Apps informed of context changes without reloading the iframe, allowing them to preserve their current state. For Apps opened in `SIDEBAR` or `EXPANDED`, Bkper communicates those changes by sending the updated App URL to the iframe when its origin remains the same:

```js
{
    type: 'bkper:app-url-changed',
    url: 'https://my-app.bkper.app?bookId=abc123&query=account:Sales',
}
```

Listen for the message in the App:

```js
const BKPER_ORIGIN = 'https://bkper.app';

window.addEventListener('message', event => {
    // Verify that the trusted Bkper parent sent the message.
    if (event.source !== window.parent || event.origin !== BKPER_ORIGIN) return;

    // Verify that this is a valid App URL update.
    const message = event.data;
    if (message?.type !== 'bkper:app-url-changed' || typeof message.url !== 'string') return;

    // Parse the updated URL, ignoring malformed URL strings.
    let nextUrl;
    try {
        nextUrl = new URL(message.url);

        // Accept only URLs belonging to this App.
        if (nextUrl.origin !== window.location.origin) return;
    } catch {
        return;
    }

    // Keep the iframe URL in sync without reloading it.
    window.history.replaceState(window.history.state, '', nextUrl);

    // Apply the validated context update.
    handleAppUrlChange(nextUrl);
});
```

`handleAppUrlChange` is App logic. The App can update internal state, notify components, refresh data, change its UI, or ignore the message. Bkper only communicates the new URL; it does not reload the iframe or apply the context inside the App.

Apps opened with `NEW_TAB` do not receive this message. Their context is set only by the URL used to open the tab.

### Available expressions

The menu URL supports these dynamic expressions:

| Expression              | Description               |
| ----------------------- | ------------------------- |
| `${book.id}`            | The current Book ID       |
| `${transactions.query}` | The current query string  |
| `${account.id}`         | The selected account ID   |
| `${account.name}`       | The selected account name |
| `${group.id}`           | The selected group ID     |
| `${group.name}`         | The selected group name   |

For the full list of accepted expressions, see the [Menu URL variables](https://bkper.com/docs/platform/apps/configuration.md#menu-url-variables) reference.

---
source: /docs/platform/apps/deploying.md

# Building & Deploying

## The deployment workflow

Sync and deployment require an attached, clean, committed Git branch whose current commit is stored remotely. Bkper-managed private Git is recommended. Apps without Git receive actionable initialization and managed-sync instructions from the CLI; external repositories must configure and push the current branch to its intended upstream.

Run the template's deterministic checks before releasing:

```bash
npm run check
```

1. **Build** — Compile your code

    ```bash
    npm run build
    ```

    This runs two build steps:
    - Client (Vite) to static assets in `dist/client/`
    - Server Worker bundle (esbuild) to `dist/server/`

    Build output includes size reporting so you can monitor bundle sizes.

2. **Sync** — Update app metadata and managed source

    ```bash
    bkper app sync
    ```

    Verifies stored source, then syncs your `bkper.yaml` configuration to Bkper — name, description, menu URLs, webhook URLs, access control, and branding. For managed source, it safely pushes the current clean, committed branch. For external source, it fetches the configured upstream and verifies that it contains the current commit. Sync does not build or deploy the app.

3. **Deploy** — Upload the local build to the platform

    ```bash
    bkper app deploy
    ```

    Deploy verifies stored source again. Managed source is pushed and Platform-verified; external source is checked against the configured upstream by the CLI. Deploy then uploads your existing pre-built code from `dist/` to the Bkper Platform. The command does not run a build, and the Platform does not prove that `dist/` was produced from the verified commit. Your app is live at `https://{appId}.bkper.app`.

The app template combines all three after source changes are committed:

```bash
npm run deploy
```

Use `npm run deploy:preview` for the preview environment.

> **Caution: Source is not deployment**
> An ordinary `git push` stores source only and never deploys. `bkper app sync` also does not deploy. Run `bkper app deploy` explicitly when the local build is ready to release.
See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for managed-source setup, cloning, access, and external Git workflows.

### Production

The default deployment target. Your app runs at `https://{appId}.bkper.app`.

```bash
bkper app deploy
```

Production serves:

```txt
Client:       https://{appId}.bkper.app
API routes:   https://{appId}.bkper.app/api/*
OpenAPI spec: https://{appId}.bkper.app/openapi.json
Events:       https://{appId}.bkper.app/events
```

### Preview

Deploy to a separate preview environment for testing before production:

```bash
bkper app deploy --preview
```

Preview URLs use a dash suffix: `https://{appId}-preview.bkper.app`. For example, an app with `id: my-app` deploys to `https://my-app-preview.bkper.app`.

Preview serves:

```txt
Client:       https://{appId}-preview.bkper.app
API routes:   https://{appId}-preview.bkper.app/api/*
OpenAPI spec: https://{appId}-preview.bkper.app/openapi.json
Events:       https://{appId}-preview.bkper.app/events
```

Preview has independent secrets and KV storage from production.

There is one app deployment per environment. `/events` is handled by the same Worker as the client assets and `/api/*` routes.

## Secrets management

Secrets are environment variables stored securely on the platform. Declare them in `bkper.yaml`:

```yaml
deployment:
    secrets:
        - EXTERNAL_SERVICE_TOKEN
```

### Setting secrets

```bash
# Set for production
bkper app secrets put EXTERNAL_SERVICE_TOKEN

# Set for preview
bkper app secrets put EXTERNAL_SERVICE_TOKEN --preview
```

You'll be prompted to enter the value.

### Listing and deleting

```bash
# List all secrets
bkper app secrets list

# Delete a secret
bkper app secrets delete EXTERNAL_SERVICE_TOKEN
```

### Accessing in code

Secrets are available as `c.env.SECRET_NAME` in your Hono handlers:

```ts
app.get('/api/data', async c => {
    const token = c.env.EXTERNAL_SERVICE_TOKEN;
    // use token
});
```

During local development, use the `.dev.vars` file instead. See [Development Experience](https://bkper.com/docs/platform/apps/development.md#local-secrets).

### KV storage

Declare KV in `bkper.yaml`:

```yaml
deployment:
    services:
        - KV
```

The platform provisions a KV namespace for your app. Access it via `c.env.KV`:

```ts
await c.env.KV.put('key', 'value', { expirationTtl: 3600 });
const value = await c.env.KV.get('key');
```

KV storage is separate between production and preview environments.

## Deployment status

Check the current state of your deployment:

```bash
bkper app status
```

## Installing on books

After deploying, install the app on specific books to activate it:

```bash
# Install on a book
bkper app install <appId> -b <bookId>

# Uninstall from a book
bkper app uninstall <appId> -b <bookId>
```

Once installed, the app's [event handlers](https://bkper.com/docs/platform/apps/event-handlers.md) receive events from that book at `/events`, and the app's [context menu](https://bkper.com/docs/platform/apps/context-menu.md) appears in the book's UI.

## Next steps

- [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) — Share private source without coupling Git pushes to deployment
- [Development Experience](https://bkper.com/docs/platform/apps/development.md) — Run the app and event delivery locally
- [App Listing](https://bkper.com/docs/platform/apps/app-listing.md) — Prepare the app for installation

---
source: /docs/platform/apps/development.md

# Development Experience

Local development uses two composable processes — the worker runtime and the client dev server — that run concurrently.

## What runs

```bash
npm run dev
```

The project template runs both processes via `concurrently`:

1. **`vite dev`** — Client dev server with HMR. Changes to Lit components reflect instantly in the browser. Configured in `client/vite.config.ts`.
2. **`bkper app dev`** — The worker runtime:
    - **Miniflare** — Simulates the single Cloudflare Worker locally.
    - **Cloudflare tunnel** — Exposes `/events` via a public URL so Bkper can route webhook events to your machine.
    - **File watching** — Server changes trigger automatic rebuilds via esbuild.

You can also run them independently: `npm run dev:client` for just the UI, or `npm run dev:server` for the local Worker.

## URLs

| Endpoint                               | URL                                         |
| -------------------------------------- | ------------------------------------------- |
| Client (Vite dev server)               | `http://localhost:5173`                     |
| Server Worker (Miniflare)              | `http://localhost:8787`                     |
| App API routes                         | `http://localhost:8787/api/*`               |
| App OpenAPI spec                       | `http://localhost:5173/openapi.json`        |
| Events (via tunnel to the same Worker) | `https://<random>.trycloudflare.com/events` |

The Vite dev server proxies `/api` and `/openapi.json` requests to `http://localhost:8787` through `client/vite.config.ts`, so the client and OpenAPI spec share the same local origin just as they do in production. The spec also remains available directly from the Worker at `http://localhost:8787/openapi.json`. The tunnel URL is automatically registered as `webhookUrlDev`, so development-mode events are routed to your local machine.

## Configuration flags

There is one local Worker. Override its port when needed:

```bash
bkper app dev --sp 8787
```

## Client configuration

The client dev server is configured in `client/vite.config.ts`. This standard Vite configuration registers local auth middleware and proxies `/api` and `/openapi.json` requests to the Worker.

### Local development authentication

During local development, the Vite dev server runs `createBkperAuthMiddleware()` from `bkper/dev`. It serves the local `/auth/refresh` endpoint used by `@bkper/web-auth`, obtaining OAuth tokens from your CLI credentials.

The separate Vite proxy configuration forwards `/api` and `/openapi.json` requests to the Miniflare Worker.

Before starting development, run:

```bash
bkper auth login   # one-time setup
```

Then `npm run dev` handles local authentication. Direct `bkper-js` calls use `auth.getAccessToken()`, while the typed app API client uses `auth.authenticatedFetch()` to attach and refresh bearer authentication.

Local outbound uses your CLI credentials when the app server or event handler calls Bkper.

If you see authentication errors in the browser, verify you're logged in:

```bash
bkper auth token   # should print a token
```

This is the canonical pattern for local development. Do not manually pass tokens or implement custom auth flows.

## Local secrets

Environment variables for local development live in a `.dev.vars` file at the project root:

```bash
# .dev.vars (gitignored)
EXTERNAL_SERVICE_TOKEN=your-token-here
```

Copy from the provided template:

```bash
cp .dev.vars.example .dev.vars
```

These variables are available as `c.env.SECRET_NAME` in your Hono handlers during development.

## KV storage

KV data persists locally in the `.mf/kv/` directory during development. This means your data survives restarts — useful for testing caching and state patterns.

```ts
// Read
const value = await c.env.KV.get('my-key');

// Write with TTL
await c.env.KV.put('my-key', 'value', { expirationTtl: 3600 });
```

See the [Cloudflare KV documentation](https://developers.cloudflare.com/kv/) for more usage patterns.

## Type generation

The `env.d.ts` file provides TypeScript types for the Worker environment — KV bindings, secrets, and other platform services. It's auto-generated based on your `bkper.yaml` configuration and checked into version control.

Rebuild it after changing services or secrets in `bkper.yaml`:

```bash
bkper app build
```

## The development loop

1. Run `npm run dev`.
2. Edit client code and use Vite HMR.
3. Edit server code and let the Worker reload.
4. Trigger events in Bkper and inspect handler responses in the activity stream.
5. Run `npm run check` before considering the change complete.

## Debugging

- **Server errors** — Check the terminal output from `bkper app dev`. Worker runtime errors appear here.
- **Event handler errors** — Check the Bkper activity stream. Click on an event handler response to see the result or error, and replay failed events.
- **Client errors** — Use browser DevTools. The Vite dev server provides source maps.

---
source: /docs/platform/apps/event-handlers.md

# Bkper Webhooks and Event Handlers

Event handlers are the code that reacts to events in your Bkper Books. When a transaction is checked, an account is created, or any other event occurs, your handler receives it and can take action — calculate taxes, sync data between books, post to external services, and more.

![Bkper Event Handler](https://bkper.com/images/bots/bkper-tax-bot/bkper-tax-bot.gif)

## How it works

1. You declare which events your app handles in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md)
2. Bkper sends an HTTP POST to your webhook URL when those events fire
3. Your handler processes the event and returns a response

On the [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md), events are routed to `/events` on your app's single Worker — including local development via tunnels. For [self-hosted](https://bkper.com/docs/platform/apps/self-hosted.md) setups, you configure the webhook URL directly.

## Agent identity

Event handlers **run on behalf of the user who installed the app**. Their transactions and activities are identified in the UI by the app's logo and name:

![Event handler agents identified in the activity stream](https://bkper.com/docs/_astro/bkper-bot-agents.CtsWIZEd.png)

## Responses

Handler responses are recorded in the activity that triggered the event. You can view and replay them by clicking the response at the bottom of the activity:

![Event handler responses in the activity stream](https://bkper.com/docs/_astro/bkper-bot-responses.UQXhqdai.png)

### Response format

Your handler must return a response in this format:

```ts
{ result?: string | string[] | boolean; error?: string; warning?: string }
```

- The `result` is recorded as the handler response in the book activity
- If you return `{ result: false }`, the response is suppressed and not recorded
- Errors like `{ error: "This is an error" }` show up as error responses

To show the full error stack trace for debugging:

```ts
try {
    // handler logic
} catch (err) {
    return { error: err instanceof Error ? err.message : String(err) };
}
```

### HTML in responses

If you return an **HTML snippet** (e.g., a link) in the result, it will be rendered in the response popup.

## Development mode

Event handlers run in _Development Mode_ when executed by the **developer or owner** of the App.

In development mode, both successful results and errors are shown as responses:

![Event handler error in development mode](https://bkper.com/docs/_astro/bkper-bot-error.4eq2AKEM.png)

You can click a response to **replay** failed executions — useful for debugging without recreating the triggering event.

To find transactions with bot errors in a book, run the query:

```
error:true
```

## Preventing loops

When your event handler creates or modifies transactions, those changes fire new events. To prevent infinite loops, check the `event.agent.id` field:

```ts
function handleEvent(event: bkper.Event) {
    // Skip events triggered by this app
    if (event.agent?.id === 'your-app-id') {
        return { result: false };
    }

    // Process the event
    // ...
}
```

This pattern is essential for any handler that writes back to the same book.

## Authentication

Platform-hosted event handlers use the same server-side Bkper API pattern as `/api/*` routes:

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

Dispatch consumes the event delivery token, strips platform headers before your Worker runs, and platform outbound auth injects the OAuth token and app agent identity on Bkper API calls.

Do not read `bkper-oauth-token`, `bkper-agent-id`, or `Authorization` headers in platform app code.

> **Note**
> During local development, events are routed through the Cloudflare tunnel started by `bkper app dev`. Local outbound uses your CLI credentials when the handler calls Bkper.
For [self-hosted](https://bkper.com/docs/platform/apps/self-hosted.md) setups, the event auth headers are sent to both `webhookUrl` and `webhookUrlDev` and must be handled directly by your infrastructure.

## Event routing pattern

On the Bkper Platform, your server Worker uses [Hono](https://hono.dev) to receive webhook calls at `/events`. A typical pattern routes events by type:

```ts
import { Bkper, Book } from 'bkper-js';

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 });
    }
});
```

## The Event object

The event payload has the following structure:

```ts
{
    /** The id of the Book associated to the Event */
    bookId?: string;

    /** The Book object associated with the Event */
    book?: {
        agentId?: string;
        collection?: Collection;
        createdAt?: string;
        datePattern?: string;
        decimalSeparator?: "DOT" | "COMMA";
        fractionDigits?: number;
        id?: string;
        lastUpdateMs?: string;
        lockDate?: string;
        name?: string;
        ownerName?: string;
        pageSize?: number;
        period?: "MONTH" | "QUARTER" | "YEAR";
        periodStartMonth?: "JANUARY" | "FEBRUARY" | "MARCH" | "APRIL"
            | "MAY" | "JUNE" | "JULY" | "AUGUST" | "SEPTEMBER"
            | "OCTOBER" | "NOVEMBER" | "DECEMBER";
        permission?: "OWNER" | "EDITOR" | "POSTER" | "RECORDER"
            | "VIEWER" | "NONE";
        properties?: { [name: string]: string };
        timeZone?: string;
        timeZoneOffset?: number;
    };

    /** The user in charge of the Event */
    user?: {
        avatarUrl?: string;
        name?: string;
        username?: string;
    };

    /** The Event agent, such as the App, Bot or Bank institution */
    agent?: {
        id?: string;
        logo?: string;
        name?: string;
    };

    /** The creation timestamp, in milliseconds */
    createdAt?: string;

    /** The event data */
    data?: {
        /** The object payload. Depends on the event type. */
        object?: any;
        /** The object previous attributes when updated */
        previousAttributes?: { [name: string]: string };
    };

    /** The unique id that identifies the Event */
    id?: string;

    /** The resource associated to the Event */
    resource?: string;

    /** The type of the Event */
    type?: EventType;
}
```

The event payload is the same structure exposed by the [REST API](https://bkper.com/docs/platform/scripts/rest-api.md). If you use TypeScript, add the [`@bkper/bkper-api-types`](https://www.npmjs.com/package/@bkper/bkper-api-types) package to your project for full type definitions.

For update events, `data.previousAttributes` contains the fields that changed and their previous values — useful for computing diffs or reacting only to specific field changes.

## Event types

Declare which events your app handles in `bkper.yaml`:

```yaml
events:
    - TRANSACTION_CHECKED
    - TRANSACTION_POSTED
    - ACCOUNT_CREATED
```

The complete API set of event types is listed below. `COMMENT_CREATED` and `COMMENT_DELETED` remain in the API for compatibility with historical Events; Comments are not available in the current Bkper PWA.

| Event | Description |
| --- | --- |
| `FILE_CREATED` | A file was attached to the book. |
| `FILE_UPDATED` | An attached file was updated. |
| `TRANSACTION_CREATED` | A draft transaction was created. |
| `TRANSACTION_UPDATED` | A transaction was updated. |
| `TRANSACTION_DELETED` | A transaction was deleted. |
| `TRANSACTION_POSTED` | A draft transaction was posted and now affects balances. |
| `TRANSACTION_CHECKED` | A posted transaction was checked (reviewed and locked). |
| `TRANSACTION_UNCHECKED` | A checked transaction was unchecked and becomes editable again. |
| `TRANSACTION_RESTORED` | A deleted transaction was restored. |
| `ACCOUNT_CREATED` | An account was created. |
| `ACCOUNT_UPDATED` | An account was updated. |
| `ACCOUNT_DELETED` | An account was deleted. |
| `QUERY_CREATED` | A saved query was created. |
| `QUERY_UPDATED` | A saved query was updated. |
| `QUERY_DELETED` | A saved query was deleted. |
| `GROUP_CREATED` | A group was created. |
| `GROUP_UPDATED` | A group was updated. |
| `GROUP_DELETED` | A group was deleted. |
| `COMMENT_CREATED` | A comment was added. |
| `COMMENT_DELETED` | A comment was deleted. |
| `COLLABORATOR_ADDED` | A collaborator was added to the book. |
| `COLLABORATOR_UPDATED` | A collaborator's permissions were updated. |
| `COLLABORATOR_REMOVED` | A collaborator was removed from the book. |
| `INTEGRATION_CREATED` | An integration was created in the book. |
| `INTEGRATION_UPDATED` | An integration was updated. |
| `INTEGRATION_DELETED` | An integration was deleted. |
| `BOOK_CREATED` | A book was created. |
| `BOOK_AUDITED` | A balances audit completed for the book. |
| `BOOK_UPDATED` | Book settings were updated. |
| `BOOK_DELETED` | The book was deleted. |

---
source: /docs/platform/apps/first-app.md

# Your First App

This tutorial walks you through building and deploying a Bkper app from scratch. For the deep reference on any topic — architecture, configuration, development, events, or deployment — follow the links in each step.

## Prerequisites

[Development Setup](https://bkper.com/docs/platform/getting-started/setup.md) — the CLI installed and authenticated.

## Walkthrough

1. **Scaffold from the template**

    ```bash
    bkper app init my-app
    cd my-app
    ```

    `bkper app init my-app` creates `./my-app` and uses `my-app` as the app id. The CLI sets your package name, URLs, and event-handler loop guards automatically. See [App Configuration](https://bkper.com/docs/platform/apps/configuration.md) for the full `bkper.yaml` reference.

2. **Install and start developing**

    ```bash
    npm install
    npm run dev
    ```

    This runs the Vite client dev server and the local Worker runtime with automatic event tunneling. See [Development Experience](https://bkper.com/docs/platform/apps/development.md) for details.

3. **Open the app**

    Visit [http://localhost:5173](http://localhost:5173). Select a book to see account balances. No OAuth setup required — the platform handles authentication.

4. **Trigger an event**

    Go to any Bkper book and check a transaction. The event handler creates a 20% draft using the original from and to Accounts. It does not affect balances unless posted. See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for the full event model.

5. **Make a change**

    Edit `server/src/events/handlers/transaction-checked.ts` and save. The Worker reloads automatically. Check another transaction to see your change.

6. **Customize your listing**

    Update `bkper.yaml` with your app's description and owner details. Replace the placeholder logos in `client/public/images/`. See [App Listing](https://bkper.com/docs/platform/apps/app-listing.md) for publishing details.

7. **Update the README**

    Edit `README.md` for end users — what the app does and how to use it. If your app exposes `/api/*` routes for users or integrators, include the app API base URL, `/openapi.json` URL, and one minimal authenticated example. Keep deeper developer docs in `AGENTS.md`.

8. **Establish shared source**

    Review the app, create its first commit, and sync it:

    ```bash
    git add .
    git commit -m "Initial app"
    bkper app sync
    ```

    Stored Git source is required before an app can sync or deploy. For this standalone app without an external remote, sync creates the recommended private Bkper-managed source and configures it as `origin`. Authorized teammates and coding agents can then clone the same codebase with `bkper app clone my-app`. See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for access rules and external source workflows.

9. **Check and deploy**

    ```bash
    npm run check
    npm run deploy
    ```

    Deployment is explicit: syncing or pushing source does not deploy the app. Your app is live at `https://my-app.bkper.app`. See [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) for preview environments, secrets, and KV.

## Next steps

- [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) — Clone and improve one private app codebase together
- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Understand the single Worker client/server structure
- [App Configuration](https://bkper.com/docs/platform/apps/configuration.md) — Full `bkper.yaml` reference
- [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) — All event types and patterns
- [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) — Preview environments and secrets

---
source: /docs/platform/apps/overview.md

# The Bkper Platform

The Bkper Platform is a complete managed environment for building, deploying, and hosting apps on Bkper. It removes infrastructure complexity so you can focus on business logic.

### Hosting

Apps are deployed to `{appId}.bkper.app` on a global edge network powered by [Cloudflare Workers for Platforms](https://developers.cloudflare.com/cloudflare-for-platforms/workers-for-platforms/). Your app runs close to your users, with zero infrastructure to manage.

Preview environments are built in — deploy to a preview URL to test before going to production.

### App APIs

The same Worker can expose app-defined `/api/*` routes. Treat those routes as the reusable contract for your app behavior:

- The bundled web client can call them.
- Scripts, external clients, and agents can call them too.
- The default template documents them with an app OpenAPI spec at `/openapi.json`.

### AI inference

When an app needs model inference, use Bkper AI by default. An authenticated app API route or event establishes the user and app identity, then platform outbound supplies authorization and usage attribution for the Worker's Bkper AI requests. The app does not need provider credentials.

See [Add Bkper AI to an App](https://bkper.com/docs/platform/apps/ai.md) for live model discovery, strict structured output, validation, and the client-to-Worker authentication flow.

### Authentication

OAuth is pre-configured. No client IDs, no redirect URIs, no consent screens to build.

- **Web client** — Use `@bkper/web-auth`: `auth.getAccessToken()`. See [App Architecture → Client authentication](https://bkper.com/docs/platform/apps/architecture.md#client-authentication).
- **Server API routes** — Send `Authorization: Bearer <token>` to `/api/*`; dispatch validates it and platform outbound injects auth for server-side Bkper API calls. See [App Architecture → Server API authentication](https://bkper.com/docs/platform/apps/architecture.md#server-api-authentication).
- **Event handlers** — Handle `/events` in the same Worker and call Bkper with server-side `new Bkper()`; dispatch/outbound handle auth and agent identity. See [Event Handlers → Authentication](https://bkper.com/docs/platform/apps/event-handlers.md#authentication).
- **Local development** — The Vite auth middleware uses your CLI credentials. See [Development Experience → Local development authentication](https://bkper.com/docs/platform/apps/development.md#local-development-authentication).

### Services

Declare the services you need in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md) and the platform provisions them:

- **KV storage** — Key-value storage for caching and state. Access via `c.env.KV` in your handlers.
- **Secrets** — Securely stored environment variables. Set via `bkper app secrets put`, access via `c.env.SECRET_NAME`.

### Developer experience

The project template composes the full development environment:

```bash
npm run dev
```

This runs two processes concurrently: `vite dev` for the client UI (HMR), and `bkper app dev` for the Worker runtime (Miniflare for `/api/*` and `/events`, plus a Cloudflare tunnel so Bkper can route webhook events to your laptop). Your entire development environment, running locally.

### Shared app source

Bkper can host one private codebase for your app. Authorized teammates and coding agents can clone it, improve it locally, and continue building from the same shared history.

Source synchronization remains separate from deployment. A Git push stores source but never builds or deploys the app.

See [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) for the collaboration workflow, access rules, and external Git options.

### Deployment

Check and deploy the app template:

```bash
npm run check
npm run deploy
```

Your app is live at `{appId}.bkper.app`. The platform handles routing, SSL, and edge distribution.

## What you'd build yourself without it

Without the platform, creating a Bkper app with a UI, event handling, and authentication requires:

| Concern                  | Without the platform                                                                    | With the platform                               |
| ------------------------ | --------------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Hosting**              | Provision servers, configure domains, SSL, CDN                                          | `bkper app deploy`                              |
| **Authentication**       | Register OAuth client, build consent screen, handle token refresh, manage redirect URIs | `auth.getAccessToken()`                         |
| **Event webhooks**       | Set up a public endpoint, configure DNS, handle JWT verification                        | Declare in `bkper.yaml`, platform routes events |
| **Local dev webhooks**   | Install ngrok or similar, manually configure tunnel URL                                 | `bkper app dev` starts tunnel automatically     |
| **Secrets**              | Set up a secrets manager, configure access                                              | `bkper app secrets put`                         |
| **KV storage**           | Deploy Redis/Memcached, manage connections                                              | Declare `KV` in `bkper.yaml`                    |
| **Preview environments** | Build a staging pipeline                                                                | `bkper app deploy --preview`                    |
| **Shared app source**    | Operate a separate private Git host                                                     | Managed source for app developers and agents    |
| **Type safety**          | Manually create type definitions                                                        | `env.d.ts` auto-generated                       |

The platform eliminates all of this. You write business logic, the platform handles infrastructure.

## Getting started

```bash
# Create a new app from the template
bkper app init my-app
cd my-app

# Install dependencies and start developing
npm install
npm run dev
```

This gives you a working app with a client UI, server API routes, and `/events` handling in one Worker — all running locally with full HMR and webhook tunneling.

## Next steps

- [Your First App](https://bkper.com/docs/platform/apps/first-app.md) — Build and deploy a complete platform app
- [Shared App Source](https://bkper.com/docs/platform/apps/shared-app-source.md) — Collaborate from one private codebase
- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Understand how platform apps are structured

---
source: /docs/platform/apps/quality.md

# App Quality Guidelines

Use these guidelines when building, changing, or reviewing a Bkper app. They complement the detailed architecture, security, and feature documentation.

After implementation, review the changed code against these guidelines before considering the work complete. Automated checks support this review but do not replace it.

## Bkper behavior

Always:

- Model financial activity as balanced resource movements between Accounts.
- Keep calculations deterministic and cover financial behavior with focused unit tests.
- Use canonical Bkper SDK and API types instead of recreating Bkper data structures.
- Add meaning with properties before introducing new structural complexity.

## User interface

Always:

- Prefer Web Awesome for all UI controls. Do not use native controls when Web Awesome provides an equivalent.
- If no equivalent exists, first compose existing Web Awesome components. Create a reusable Lit web component only when necessary, keeping any native controls encapsulated within it.
- Style with Bkper design tokens instead of ad-hoc design constants.
- Support the active Bkper light or dark theme.
- Keep interactions accessible and provide clear loading, empty, and error states.

Apps opened from a Book context menu should feel like part of the Book. Keep their layout focused, make them work in the configured sidebar or expanded width, and preserve context when the Book URL changes.

## Startup

Always render the app shell or a meaningful loading state before waiting for authentication, API calls, or other initialization. Start asynchronous work after the first render and update the interface as results arrive.

Prefer loading only the client code and Web Awesome components needed for the initial experience.

## API contracts

When an app exposes its own HTTP API:

- Define and publish its OpenAPI contract at `/openapi.json`.
- Generate client types from that contract.
- Call the API through the generated typed boundary rather than duplicating request or response types.
- Validate untrusted request data at the server boundary.
- Keep published routes backward compatible or introduce a new API version for breaking changes.

UI-only and event-only apps without an app-owned HTTP API do not need to add one. Direct calls to Bkper should use the canonical Bkper SDK and API types.

## Focused modules

Prefer modules with one clear responsibility, high cohesion, and few dependencies.

- Components render state and communicate user intent. They should delegate API and Bkper operations to client API or service modules.
- HTTP routes and event handlers adapt transport concerns and delegate app behavior.
- Business modules contain domain decisions without depending on UI, HTTP, storage, or external-service details.
- Connectors isolate external APIs and storage concerns from business behavior.
- Add a repository or another layer when connection or storage complexity justifies it, not by default.
- Avoid layers that only forward calls without creating a useful boundary.

Split a module when unrelated behavior changes for different reasons or when mixed responsibilities make it difficult to understand or test.

## Security

Keep the security boundary simple and explicit:

- Authorize sensitive app operations on the server.
- Validate untrusted requests, events, and browser messages at their boundaries.
- Keep application secrets and privileged credentials out of client bundles.
- Access only the Book data needed for the operation.
- Make event handling safe to retry and prevent event loops.

See [App Security](https://bkper.com/docs/platform/apps/security.md) and [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md) for implementation details.

## Verification

After implementation:

1. Review the changed code against these guidelines.
2. Run the app's deterministic checks, including unit tests, typechecking, and production builds.
3. Confirm generated API types and contract snapshots are current when the API changed.
4. Verify user interfaces in their intended Book context, including first rendering, loading, errors, theme, and configured width.

App reviews should report concrete findings with file locations and suggested fixes. If no issues are found, a short confirmation is enough.

## Next Steps

- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Client, server, API, and event structure.
- [App Security](https://bkper.com/docs/platform/apps/security.md) — Authentication and authorization boundaries.
- [Context Menu](https://bkper.com/docs/platform/apps/context-menu.md) — Embedded Book context and open modes.
- [Development Experience](https://bkper.com/docs/platform/apps/development.md) — Local development and deterministic checks.

---
source: /docs/platform/apps/security.md

# App Security

Bkper and each platform app have separate security responsibilities. This guide explains the platform authentication boundary and common authorization checks an app must enforce.

## Authentication and authorization

Authentication identifies the Bkper user making a request. The platform handles this flow for deployed apps.

Authorization determines whether that user may perform a specific operation.

See [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) for the client and server authentication flow.

## Authorize app operations

Platform authentication identifies the Bkper user and provides outbound authentication for server-side Bkper requests. Your app must still decide which authenticated users may perform each operation. Protect sensitive data and actions in the server API; client-side checks may improve the UI, but they are not an authorization boundary.

### Restrict an internal app by user domain

For an app intended only for people in one organization, authorize the authenticated user's hosted domain:

```ts
const ALLOWED_DOMAIN = 'example.com';

const user = await context.bkper.getUser();
const domain = user.getHostedDomain()?.toLowerCase();

if (domain !== ALLOWED_DOMAIN) {
    return c.json(buildApiError('FORBIDDEN', 'This app is restricted to your organization'), 403);
}
```

### Authorize a Book-backed operation

When an operation acts on a Book, use an explicit permission allowlist appropriate to that operation. For an operation that requires edit access:

```ts
import { Permission } from 'bkper-js';

const EDIT_PERMISSIONS: readonly Permission[] = [Permission.EDITOR, Permission.OWNER];

const book = await context.bkper.getBook(bookId);

if (!EDIT_PERMISSIONS.includes(book.getPermission())) {
    return c.json(
        buildApiError('FORBIDDEN', 'Editor or owner permission required for this operation'),
        403
    );
}
```

Read, posting, and other operations may require different policies. Choose the minimum authorization appropriate to the behavior instead of treating every authenticated user as authorized.

### Require app installation

Having permission to access a Book does not mean the app is installed in that Book. If an app is only supposed to be used with Books where it is installed, verify installation:

```ts
const APP_ID = 'my-app';

const book = await context.bkper.getBook(bookId);
const installedApps = await book.getApps();
const isInstalled = installedApps.some(app => app.getId() === APP_ID);

if (!isInstalled) {
    return c.json(buildApiError('FORBIDDEN', 'This app is not installed in this Book'), 403);
}
```

## Next Steps

- [App Quality Guidelines](https://bkper.com/docs/platform/apps/quality.md) — Review cross-cutting app quality and security expectations.
- [App Architecture](https://bkper.com/docs/platform/apps/architecture.md) — Understand client and server authentication flows.
- [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md#setting-secrets) — Store production and preview secrets.
- [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md#authentication) — Understand authentication for platform and self-hosted events.

---
source: /docs/platform/apps/self-hosted.md

# Bkper Self-Hosted Webhooks

The [Bkper Platform](https://bkper.com/docs/platform/apps/overview.md) handles hosting, authentication, and deployment for you. However, you can host event handlers on your own infrastructure if you have specific requirements — existing cloud setup, compliance constraints, or legacy apps.

> **Tip**
> Use the Bkper Platform unless you have a specific reason to self-host. It eliminates the need to manage authentication, secrets, hosting, and deployment yourself.
## Cloud Functions

A Bkper event handler running on [Google Cloud Functions](https://cloud.google.com/functions/) receives authenticated calls from the `bkper-hrd@appspot.gserviceaccount.com` service account. You need to grant this service account the [Cloud Functions Invoker IAM role](https://cloud.google.com/functions/docs/securing/managing-access-iam) (`roles/cloudfunctions.invoker`).

Set the production endpoint in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md):

```yaml
webhookUrl: https://us-central1-my-project.cloudfunctions.net/events
```

### Authentication

An OAuth Access Token **of the user who installed the app** is sent to the production `webhookUrl` endpoint in the `bkper-oauth-token` HTTP header, along with the agent identifier in `bkper-agent-id`, on each event. Your handler uses this token to call the API back on behalf of the user.

Both production (`webhookUrl`) and development (`webhookUrlDev`) endpoints receive OAuth tokens in the `bkper-oauth-token` header.

### Throughput and scaling

Event throughput can be high, especially when processing large batches. Set the [max instance limit](https://cloud.google.com/functions/docs/max-instances#setting_max_instances_limits) — usually **1-2 is enough**. When the function returns `429 Too Many Requests`, the event is automatically retried with incremental backoff until it receives an HTTP `200`.

### Response format

The function response must follow the standard format:

```ts
{ result?: any, error?: any }
```

See [Event Handlers](https://bkper.com/docs/platform/apps/event-handlers.md#response-format) for details on response handling.

### Considerations

- Execution environment is subject to [Cloud Function Quotas](https://cloud.google.com/functions/quotas) — quota counts against the developer account, not the end user
- Recommended for scenarios where event throughput exceeds **1 event/second/user** and processing can be handled asynchronously
- Can be combined with context menus built with [Apps Script HTML Service](https://developers.google.com/apps-script/guides/html) or any other UI infrastructure

---

## Generic Webhooks

You can host event handlers on any infrastructure — other cloud providers, containers, on-premise servers.

Configure the same `webhookUrl` property in [`bkper.yaml`](https://bkper.com/docs/platform/apps/configuration.md):

```yaml
webhookUrl: https://my-server.example.com/bkper/events
```

### Authentication

Calls to the production webhook URL are signed with a JWT token using the [Service to Function](https://cloud.google.com/functions/docs/securing/authenticating#service-to-function) method. You can verify this token to assert the identity of the Bkper service.

> **Note**
> Cloud Functions handles JWT verification automatically. For other infrastructure, you need to implement verification yourself. We strongly recommend Cloud Functions for this reason.
### Retry behavior

If your infrastructure returns an HTTP `429` status, the event is automatically retried with incremental backoff until it receives an HTTP `200`. Use this to handle temporary overload gracefully.

---
source: /docs/platform/apps/shared-app-source.md

# Shared App Source

Bkper-managed app source gives your team and coding agents one shared private codebase for a Bkper app. Authorized app developers can clone the same repository, improve it locally, and continue from one shared history without setting up a separate Git host.

Every app sync and deployment requires clean, committed source stored in a durable Git remote. Bkper-managed source is the recommended default for development collaboration. It does not automatically build or deploy your app.

## Who can access the source

The app owner and users matched by the `developers` field in `bkper.yaml` can read and update managed source. This includes configured domain patterns such as `*@example.com`.

App users and Book collaborators do not receive source access unless they also match the app's developer policy.

## Start a shared codebase

A standalone app becomes eligible for Bkper-managed source when:

- `bkper.yaml` is at the root of its Git repository.
- The current branch is `main` for the first managed sync.
- The working tree is clean and has at least one commit.
- No Git remote is configured.

If the app source already exists but is not yet versioned, do not run `bkper app init` again. From the directory containing `bkper.yaml`, initialize it and review the files before committing. Update `.gitignore` first so local secrets, dependencies, and build output are not staged.

```bash
git init -b main

# Review files and update .gitignore before staging
git status --short

git add .

# Verify exactly what will be committed
git status --short
git commit -m "Initial app"
bkper app sync
```

For a new app, `bkper app init` initializes Git on `main`, but it does not create the first commit. Review and commit the app before its first sync:

```bash
bkper app init my-app
cd my-app

# Review the generated app, then commit it
git add .
git commit -m "Initial app"

bkper app sync
```

For an eligible new or existing app, `bkper app sync` creates its private Bkper-managed source and configures it as `origin`. The same command also syncs app metadata from `bkper.yaml`.

## Clone and continue together

Any authorized app developer can start from the shared codebase:

```bash
bkper app clone <appId>
cd <appId>
npm install
```

`bkper app clone` copies the repository but does not install dependencies or run repository scripts. A teammate or coding agent can then work in the local clone, run the project's checks, commit changes, and push them back to the shared repository. Another authorized developer can pull or clone that history and continue the work.

Keep agent instructions such as `AGENTS.md` in the repository so every teammate and coding agent starts with the same project context and safety rules.

## Source synchronization is not deployment

Source storage and app deployment are separate operations. Apps without a Git repository cannot sync or deploy; the CLI provides the initialization and managed-sync steps needed to establish source safely.

| Action             | What it does with source                                                       | Does it deploy? |
| ------------------ | ------------------------------------------------------------------------------ | --------------- |
| `git push`         | Stores committed source in the managed repository.                             | No              |
| `bkper app sync`   | Safely pushes managed source, then syncs app metadata from `bkper.yaml`.       | No              |
| `npm run build`    | Creates local build output in `dist/`.                                         | No              |
| `bkper app deploy` | Pushes and verifies the managed commit, then uploads the existing local build. | Yes             |

An ordinary Git push never deploys. `bkper app deploy` also does not run a build, so build locally before deploying the result you intend to release. The CLI verifies the stored source commit but does not prove that the local `dist/` output was built from it.

Managed sync and deploy require a clean, committed working tree and use fast-forward safety checks. External sync and deploy require the current branch to track an upstream containing the current clean commit. The CLI does not automatically commit, merge, rebase, force-push, reset, discard files, choose an external remote, or push to an external provider.

## External Git and monorepos

Bkper-managed source is optional. Existing workflows remain external when:

- the app already has a GitHub, GitLab, or other provider remote; or
- `bkper.yaml` is inside a monorepo rather than at the repository root.

The current branch must have a configured upstream containing the commit being synced or deployed. If no upstream is configured, choose the intended provider remote and store the branch explicitly:

```bash
git push --set-upstream <remote> <branch>
```

The CLI fetches and verifies the upstream without pushing or changing the working tree. Clone external apps from their provider. `bkper app clone` is for Bkper-managed source only.

Moving an existing standalone app from an external provider is intentional: the CLI never removes or renames an existing remote. Before changing remotes, make sure every branch and tag you want to preserve is available locally and the current `main` branch is clean and committed. Removing all external remotes and running `bkper app sync` then activates managed source for an eligible app.

The `repoUrl` field in `bkper.yaml` is app-listing metadata. It does not select managed or external source mode.

## Next steps

- [Your First App](https://bkper.com/docs/platform/apps/first-app.md) — Scaffold an app and establish its shared source
- [Building & Deploying](https://bkper.com/docs/platform/apps/deploying.md) — Build, sync, preview, and deploy explicitly
- [CLI](https://bkper.com/docs/platform/tools/cli.md) — Install the CLI and review its app-development workflows
- [Coding Agents](https://bkper.com/docs/ai/coding-agents.md) — Give coding agents the Bkper and project context they need
