
# API Event Sourcing

Every document in the platform tracks its full change history via event sourcing. Each document has an optional `changes` array that records every modification — who made it, when, and what the MongoDB modifier was.

By default, API endpoints **exclude** the `changes` array from responses. Include it on demand when you actually need the change history.

→ See also: [Local-First Architecture](/developer/local-first) for the broader sync model.

## Centralized API routing

All API routes across both the `backoffice` and `applications` apps are now served through a **centralized Hono router** mounted at `/api/[...path]`. Rather than defining individual SvelteKit `+server.ts` files for each endpoint, new routes are registered in the shared `@smallstack/server` package.

### How it works

Each app mounts the shared router in `src/routes/api/[...path]/+server.ts`. Incoming requests are forwarded to the Hono router after stripping the `/api` prefix — so a request to `/api/health` is matched against the `/health` route inside the router:

```
/api/health  →  Hono router: /health
/api/projects/proj_abc/types/type_xyz/data  →  Hono router: /projects/proj_abc/types/type_xyz/data
```

### Adding new routes

Register new routes in `packages/server/src/api/router.ts` using the shared `Hono` instance returned by `createApiRouter()`. Do **not** create new SvelteKit `+server.ts` files under `apps/*/src/routes/api/` for individual endpoints.

```typescript
// packages/server/src/api/router.ts
router.get("/my-new-endpoint", myHandler);
```

Handler files live under `packages/server/src/api/routes/`.

### Authentication middleware

The `requireUser` middleware in `packages/server/src/api/middleware.ts` can be applied to any route that requires an authenticated session. It calls `ctx.getCurrentUser()` and returns `401 Unauthorized` if no user is present:

```typescript
import { requireUser } from "@smallstack/server";

router.get("/protected-route", requireUser, myProtectedHandler);
```

### Bypassing SvelteKit middleware for Hono-handled paths

Certain SvelteKit server hooks (such as `currentUser`) are skipped for paths that Hono handles directly. The list of bypassed paths is maintained in `HONO_HANDLED_PATHS` (exported from `packages/server/src/api/types.ts`). Add any path that Hono handles entirely on its own to this constant to prevent unnecessary SvelteKit middleware execution:

```typescript
// packages/server/src/api/types.ts
export const HONO_HANDLED_PATHS = ["/api/health"];
```

## The `changes` property

Each entry in the `changes` array represents one write operation:

```typescript
interface EventSource {
  userId: string;       // Who made the change
  timestamp: number;    // When (Unix ms)
  baseRevision: number; // Document revision before this change
  modifier: {           // MongoDB update modifier
    $set?: Record<string, unknown>;
    $unset?: Record<string, unknown>;
    // ... other MongoDB operators
  };
}
```

The `revision` field on the document increments with each change. The platform uses `baseRevision` to detect conflicts when multiple clients modify the same document concurrently.

## API usage

### Default (changes excluded)

```http
GET /api/projects/{projectId}/types/{typeId}/data
GET /api/projects/{projectId}/types/{typeId}/data/{id}
```

Response:
```json
{
  "id": "507f1f77bcf86cd799439011",
  "name": "Sample Document",
  "revision": 3,
  "createdAt": 1640995200000,
  "updatedAt": 1640995800000
}
```

### Include change history

```http
GET /api/projects/{projectId}/types/{typeId}/data?includeChanges=true
GET /api/projects/{projectId}/types/{typeId}/data/{id}?includeChanges=true
```

Response:
```json
{
  "id": "507f1f77bcf86cd799439011",
  "name": "Sample Document",
  "revision": 3,
  "createdAt": 1640995200000,
  "updatedAt": 1640995800000,
  "changes": [
    {
      "userId": "user123",
      "timestamp": 1640995200000,
      "baseRevision": 0,
      "modifier": { "$set": { "name": "Initial Document" } }
    },
    {
      "userId": "user456",
      "timestamp": 1640995800000,
      "baseRevision": 2,
      "modifier": { "$set": { "name": "Sample Document" } }
    }
  ]
}
```

### Project-wide audit trail

For a unified, filterable view of all changes across every entity type in a project, use the dedicated audit trail endpoint. This is the recommended approach for building audit logs and history views rather than fetching `includeChanges=true` per entity type.

```http
GET /api/projects/{projectId}/audit-trail
```

#### Query parameters

| Parameter    | Type   | Default | Description |
|--------------|--------|---------|-------------|
| `page`       | number | `1`     | Page number (1-based) |
| `limit`      | number | `50`    | Results per page. Maximum `200`. |
| `userId`     | string | —       | Filter by the ID of the user who made the change |
| `typeId`     | string | —       | Filter by entity type ID |
| `from`       | number | —       | Timestamp range start in milliseconds (Unix ms) |
| `to`         | number | —       | Timestamp range end in milliseconds (Unix ms) |
| `changeType` | string | —       | One of `created`, `updated`, or `deleted` |

#### Pagination

The response includes pagination metadata alongside the result set:

```json
{
  "items": [ /* ChangeFeedItem[] */ ],
  "total": 340,
  "page": 1,
  "limit": 50
}
```

- Default page size is **50** entries.
- Maximum page size is **200** entries. Requests with `limit` above this value are clamped to `200`.
- Use `total` and `limit` to derive the number of pages: `Math.ceil(total / limit)`.

#### Example: first page with filters

```http
GET /api/projects/proj_abc/audit-trail?page=1&limit=50&changeType=updated&from=1700000000000&to=1710000000000
```

Response:
```json
{
  "items": [
    {
      "entityId": "507f1f77bcf86cd799439011",
      "entityTitle": "Sample Document",
      "entityTypeId": "type_xyz",
      "entityTypeName": "Contract",
      "changeDescription": "changes.feed.updated.single",
      "userId": "user123",
      "userDisplayName": "Alice Example",
      "timestamp": 1709500000000
    }
  ],
  "total": 12,
  "page": 1,
  "limit": 50
}
```

#### Access control

The endpoint enforces the same RLS (Row-Level Security) read permissions as other project data endpoints. Only changes on entities the requesting user is permitted to read are included in the response.

## Schema and sample-data endpoints

### Authentication requirement

The schema and sample-data endpoints require an authenticated session. Unauthenticated requests receive a `401 Unauthorized` response.

| Endpoint | Method | Auth required |
|---|---|---|
| `/api/projects/{projectId}/types/{typeId}/schema` | GET | ✓ Yes |
| `/api/schemas/projects/{projectId}/types/{typeId}` | GET | ✓ Yes |
| `/api/projects/{projectId}/types/{typeId}/sample-data` | GET | ✓ Yes |
| `/api/projects/{projectId}/types/{typeId}/sample-data` | POST | ✓ Yes |

**401 response example:**

```http
GET /api/projects/proj_abc/types/type_xyz/schema
```

```json
HTTP/1.1 401 Unauthorized

"Unauthorized"
```

Ensure you have a valid session before calling these endpoints. In the backoffice, sessions are managed automatically via the authentication flow. When calling these endpoints from external clients or scripts, pass a valid session cookie or authorization header as appropriate.

## Application versioning endpoints

Applications support an explicit publish/version workflow. Publishing creates an immutable `ApplicationVersion` snapshot of the current application state, which can then be pinned to a domain for stable deployments.

### Publish an application

Creates an immutable `ApplicationVersion` snapshot from the current application state. The version number auto-increments per application, starting at 1. Publishing also updates the source `Application` document with `publishedVersionNumber`, `publishedVersionId`, and `publishedAt` metadata.

Requires **update** permission on the application (RLS `UPDATE` permission is checked against the application's `access` field).

```http
POST /api/projects/{projectId}/applications/{appId}/publish
```

**Request body:** none

**Response:** the newly created `ApplicationVersion` document.

```json
{
  "id": "ver_abc123",
  "applicationId": "app_xyz",
  "versionNumber": 3,
  "publishedBy": "user_456",
  "title": { "en": "My App" },
  "routes": [ /* ... */ ],
  "createdAt": 1710000000000
}
```

**Notes:**

- The version number is derived from the `publishedVersionNumber` field on the `Application` document: `nextVersionNumber = (publishedVersionNumber ?? 0) + 1`. The `baseRevision` optimistic-concurrency check on the subsequent `Application` patch ensures only one concurrent publish succeeds.
- `ApplicationVersion` documents are immutable — they are never updated or deleted after creation.
- The source `Application` document is updated atomically to reflect the new `publishedVersionNumber`, `publishedVersionId`, and `publishedAt` timestamp.
- `serverConfiguration` is included in the snapshot for completeness but is not served to public clients.

### List application versions

Returns the full version history for an application, sorted by `versionNumber` descending (most recent first).

Requires **read** permission on the application (RLS `READ` permission is checked against the application's `access` field).

```http
GET /api/projects/{projectId}/applications/{appId}/versions
```

**Response:** array of `ApplicationVersion` documents, newest first.

```json
[
  {
    "id": "ver_abc123",
    "applicationId": "app_xyz",
    "versionNumber": 3,
    "publishedBy": "user_456",
    "createdAt": 1710000000000
  },
  {
    "id": "ver_abc122",
    "applicationId": "app_xyz",
    "versionNumber": 2,
    "publishedBy": "user_123",
    "createdAt": 1709900000000
  }
]
```

### Domain version pinning

When assigning a domain to an application, you can optionally pin it to a specific `ApplicationVersion` using the `targetVersionId` field on the assign endpoint. When absent (or explicitly cleared), the domain serves the latest published version.

```http
POST /api/projects/{projectId}/domains/{domainId}/assign
```

**Request body:**

```json
{
  "targetType": "application",
  "targetId": "app_xyz",
  "targetVersionId": "ver_abc122"
}
```

To remove a version pin and revert to always-latest behaviour, send `targetVersionId` as `null` or omit the key entirely:

```json
{
  "targetType": "application",
  "targetId": "app_xyz",
  "targetVersionId": null
}
```

When `targetVersionId` is present and truthy it is stored on the `ProjectDomain` document via `$set`. When the key is present in the request body but falsy (`null` or `undefined`), the field is removed from the document via `$unset`, reverting to latest-version behaviour. When the key is absent from the request body altogether, the existing pin (if any) is left unchanged.

## When to use `includeChanges=true`

Only request change history when you actually need it. Documents with long histories can be 50–90% larger with changes included.

**Use `includeChanges=true` when:**
- Fetching change history for a specific, known document
- Implementing conflict resolution UI for a single entity
- Debugging sync issues on a particular record
- Running event sourcing analysis scoped to one entity type

**Use the audit trail endpoint instead when:**
- Building a project-wide audit log or history view
- Filtering changes across multiple entity types simultaneously
- Presenting change history to end users in the backoffice settings

**Do not use either for:**
- Standard data fetching and display
- SignalDB incremental sync (the sync protocol handles changes separately)
- List endpoints where you only need the current state

## Write operations

POST, PATCH, and DELETE operations always return the complete document including the `changes` array — this is needed for the local-first conflict detection model.

## User schema notes

The `User` document (and its session counterpart `LocalAuthUser`) tracks recently visited projects via the `recentProjects` field rather than a single `lastProjectId`. This field is a map of `projectId → timestamp (ms)` recording when the user last opened each project, and is capped at 50 entries (oldest entries are evicted first when the cap is reached).

```typescript
interface User {
  // ...
  /** Map of projectId → timestamp (ms) tracking when the user last opened each project */
  recentProjects?: Record<string, number>;
  // ...
}
```

When a user navigates into a project, a `PUT /api/me/last-project` call updates this map. The modifier applied to the stored document looks like:

```json
{ "$set": { "recentProjects": { "proj_abc": 1710000000000, "proj_xyz": 1709999000000 } } }
```

Each write replaces the full `recentProjects` map with a sorted, capped snapshot, so the change history for this field reflects the complete map state at each point in time — consistent with how all other document fields are tracked via event sourcing.

## Related

- [Local-First Architecture](/developer/local-first) — how SignalDB uses revision tracking and conflict detection
