
# Applications

An **application** is the top-level unit in the platform. It defines what your users see — the pages, their layout, the widgets on each page, and who is allowed to access what.

Applications live in configuration rather than code. You can add pages, rearrange widgets, and change access rules without redeploying anything.

> **Badge:** Creating your first application awards you the **First Application Created** badge. You can view your earned badges from your user profile.

## Application structure

```mermaid
graph TD
    A[Application] --> B[Pages / Routes]
    A --> C[Compound Widgets]
    A --> D[Configuration]
    B --> E[Page Meta]
    B --> F[Layout]
    B --> G[Content Widgets]
    B --> H[Access Rules]
```

**Application** — the container. Has a name, a set of pages, optional custom domains, and PWA settings.

**Pages (Routes)** — each page is a URL path (`/`, `/contacts`, `/contacts/:id`). A page has a layout, content widgets, SEO meta, and access rules.

**Compound Widgets** — reusable widget compositions that can be used as layouts across multiple pages (e.g., a header+sidebar shell).

**Configuration** — key-value pairs for application-wide settings like the login path, theme, or feature flags.

### Authentication tenant (`authTenantId`)

Every application has an `authTenantId` field that controls **which group of users can log in** to the application. You choose this when creating the application and it cannot be changed afterwards through the application settings UI.

There are two options:

| Option | `authTenantId` value | Who can log in |
|---|---|---|
| **App users** | The project's own tenant ID | End users who have registered directly with this project's application |
| **Employees (staff)** | The backoffice project tenant ID | Members of the backoffice project (i.e., your team / employees) |

When you click **Add Application** in **Settings → Applications**, the dialog presents an **Authentifizierung** (Authentication) dropdown with these two choices. The default is **App-Benutzer** (App users), which means end users registered with your project can log in. Selecting **Mitarbeiter** (Employees) restricts login to backoffice project members — useful for internal tooling or staff-facing dashboards.

If `authTenantId` is not explicitly set when an application is created via the API, it defaults to the project ID (i.e., App users).

## How routing works

Routes support static paths, dynamic parameters, and wildcards:

| Pattern | Example URL | Notes |
|---|---|---|
| `/home` | `/home` | Exact static match |
| `/users/:id` | `/users/123` | Parameter `id` is available in widgets |
| `/projects/:projectId/tasks/:taskId` | `/projects/abc/tasks/xyz` | Multiple parameters |
| `/blog/*` | `/blog/anything/here` | Wildcard |

When a user navigates to a URL, the router finds the matching route pattern, extracts any parameters, and renders the page's widget tree. Parameters are accessible to all widgets on that page.

### API routes (`/api/*`)

All requests under `/api/*` are handled by a **Hono router**, not by SvelteKit page routing. The Hono router is mounted at `src/routes/api/[...path]/+server.ts` and receives every HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD).

When a request arrives at `/api/<path>`, the SvelteKit handler strips the `/api` prefix before forwarding it to the Hono router. This means routes are registered on the Hono router without the `/api` prefix — for example, the health endpoint is registered as `/health` and is accessible at `/api/health`.

**Important:** Requests matched by the Hono router bypass the standard SvelteKit auth/session middleware. The auth/session middleware checks whether the current path is in the list of Hono-handled paths (`HONO_HANDLED_PATHS`) and skips user resolution for those paths entirely. Authentication for API routes is handled inside the Hono router itself using dedicated middleware (see `requireUser` below).

#### Built-in API endpoints

| Method | Path | Auth required | Description |
|---|---|---|---|
| `GET` | `/api/health` | No | Returns `ok` with HTTP 200. Lightweight connectivity probe. |
| `HEAD` | `/api/health` | No | Returns HTTP 200 with no body. Suitable for load balancer health checks. |

#### Authentication middleware

API routes that require an authenticated user use the `requireUser` Hono middleware from `@smallstack/server`. This middleware:

1. Calls `ctx.getCurrentUser()` to resolve the current user from the session.
2. Returns `401 Unauthorized` if no user is found.
3. Stores the resolved user in the Hono context (`c.get("user")`) for downstream handlers.

Protected routes should be composed with `requireUser` before their handler:

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

router.get("/some-protected-route", requireUser, (c) => {
  const user = c.get("user");
  return c.json({ userId: user.id });
});
```

## Pages and layouts

Each page can use a **layout compound widget** — a pre-built widget composition that provides the page shell (e.g., a navbar + sidebar + content area). The page's own content widgets are injected into the content slot of that layout.

Pages you create get an optional SEO meta configuration: title, description, Open Graph tags, and Twitter card settings.

## Access control

Each page can have access rules:

- **Public** — no authentication required
- **Authenticated** — users must be logged in
- **Permission-based** — users must have a specific permission

Unauthenticated users hitting a protected page are redirected to the login path configured in the application settings.

## Project context

When an application loads, the platform resolves the **project** that the application belongs to (via the application's `tenantId`) and exposes a public project context alongside the application data. This context is available throughout the application layout and provides the following properties:

| Property | Type | Description |
|---|---|---|
| `id` | `string` | The unique identifier of the project |
| `aiEnabled` | `boolean` | Whether AI features are enabled for this project |
| `hasReadAccess` | `boolean` | Whether the current user has read access to the project |
| `hasWriteAccess` | `boolean` | Whether the current user has write access to the project |
| `lang` | `string` | The detected language for the current request (see below) |

These properties are computed server-side on each layout load and reflect the current user's permissions at the time of the request.

### Language detection

The `lang` property is returned by the application load function and reflects the user's preferred language as detected from the browser's `Accept-Language` request header. It is used to initialize i18n during server-side rendering (SSR), ensuring the correct language is applied before the page reaches the client.

The following language codes are supported:

| Code | Language |
|---|---|
| `de` | German |
| `en` | English |
| `es` | Spanish |
| `fr` | French |
| `it` | Italian |
| `pl` | Polish |

The detection logic reads the `Accept-Language` header, iterates through the browser's stated preferences in order, and returns the first match against the supported language list. If no supported language is found — or if the header is absent — the language falls back to **`de`** (German).

On the client side, the detected `lang` value is not used after hydration; the browser's own language detection takes over from that point.

### AI feature availability

The `aiEnabled` flag on the project context controls whether AI-powered features (such as AI agents, smart import, and action summaries) are accessible within the application. When `aiEnabled` is `false`, AI feature components display an informational message instead of their normal content.

Users with `hasWriteAccess` on the project will additionally see a link to navigate to the AI settings page to enable the feature. Users without write access see the unavailability message only.

To enable AI for a project, go to **Project → Settings → AI** and toggle the AI setting on.

## PWA support

Applications can be installed as Progressive Web Apps. Configure the app name, short name, theme color, and icons under **Application → Settings → PWA**. The platform generates the web manifest automatically.

## Error tracking

Both the `applications` and `backoffice` apps support client-side error tracking via **Bugsink**, using the Sentry SDK under the hood. Error tracking is initialized automatically on the client side when the required environment variable is present.

> **Note:** The error tracking backend has changed from **Bugfender** to **Bugsink**. If you were previously using `PUBLIC_BUGFENDER_API_KEY`, you must update your environment configuration as described below.

### Required environment variable

| Variable | Description |
|---|---|
| `PUBLIC_BUGSINK_DSN` | The DSN (Data Source Name) of your Bugsink project. Provided by your Bugsink instance under **Project → Settings → Client Keys**. |

Set this variable in your `.env` file (or your deployment environment) for each app that requires error tracking:

```env
PUBLIC_BUGSINK_DSN=https://<key>@bugsink.example.com/<project-id>
```

When `PUBLIC_BUGSINK_DSN` is set, the platform initializes the Sentry SDK with the provided DSN on startup. Unhandled errors and promise rejections are captured automatically. Users can also trigger a manual bug report from error banner components in the UI, which sends the error and its context directly to Bugsink.

If `PUBLIC_BUGSINK_DSN` is not set or is empty, error tracking is silently skipped and errors fall back to `console.error`.

## Analytics

The platform integrates with **[Plausible](https://plausible.io)** for privacy-friendly usage analytics.

- **Backoffice (apps.smallstack.com):** Analytics are active when `PUBLIC_PLAUSIBLE_SRC` is set.
- **Customer applications:** Analytics are active when `PUBLIC_PRODUCTION` is `"true"` and `plausibleSrc` is configured in the application settings.

### Environment variables (backoffice)

| Variable | Required | Default | Description |
|---|---|---|---|
| `PUBLIC_PLAUSIBLE_SRC` | Yes (to enable analytics) | — | The unique Plausible script URL from your Plausible dashboard (e.g. `https://plausible.io/js/pa-xxx.js`). Analytics are disabled if not set. |

### Application configuration (customer apps)

| Setting | Required | Default | Description |
|---|---|---|---|
| `plausibleSrc` | Yes (to enable analytics) | — | The Plausible script URL for this application. |
| `plausibleDomain` | No | — | Optional `data-domain` override for self-hosted Plausible instances. |

These settings can be configured per-application in the backoffice under **Settings > Applications**.

### How it works

The layout dynamically injects the Plausible script into `<head>` on the client side. Before the script loads, a `window.plausible` event queue and `plausible.init()` stub are set up so that events fired during the script's load are not lost. Plausible's new snippet format uses a unique script URL per site that encodes the domain, so a separate `data-domain` attribute is no longer required.

### Custom events

PWA install actions are tracked as custom Plausible events. The event name follows the pattern:

```
{appTitle} PWA Install - {deviceType} - {installType}
```

For example: `My App PWA Install - Mobile - prompt`.

Custom events can be queried in your Plausible dashboard under **Custom Events**.

### Self-hosted Plausible

If you run a self-hosted Plausible instance, set `PUBLIC_PLAUSIBLE_SRC` to your instance's script URL and configure `plausibleDomain` in application settings:

```
PUBLIC_PLAUSIBLE_SRC=https://plausible.mycompany.com/js/script.js
```

## Versioning & Publishing

The platform uses an explicit **publish** step to control which version of an application is served to end users. Editing an application in the backoffice does not immediately affect what users see — you must publish a version to make changes live.

### How versioning works

Each time you publish, the platform creates an **immutable ApplicationVersion snapshot** — a point-in-time copy of the application's full configuration. The following fields are included in every snapshot:

| Field | Description |
|---|---|
| `routes` | All page definitions including widgets, layouts, access rules, and SEO meta |
| `pwaConfiguration` | Progressive Web App manifest settings |
| `compoundWidgets` | All reusable widget compositions defined on the application |
| `configuration` | Application-wide key-value configuration (e.g. login path, theme) |
| `serverConfiguration` | Server-side key-value configuration (stored in the snapshot but not served publicly) |
| `optIns` | Opt-in consent declarations |
| `sitemapConfiguration` | Sitemap generation settings |
| `registrationMode` | Whether registration is open, disabled, or invite-only |
| `authTenantId` | The authentication tenant linked to the application |
| `title` | The application display name |

Version snapshots are **never modified or deleted** after creation.

Version numbers are **auto-incrementing integers scoped per application**, starting at 1. Version 1 is the first published snapshot, version 2 is the next, and so on.

### Application publish fields

Publishing a version updates three fields on the `Application` document:

| Field | Type | Description |
|---|---|---|
| `publishedVersionNumber` | `number` | The version number of the latest published snapshot |
| `publishedVersionId` | `string` | The ID of the latest `ApplicationVersion` document |
| `publishedAt` | `number` | Unix timestamp (ms) of when the last publish occurred |

These fields are used by the UI to show publish status and detect unpublished changes. The **"Unpublished changes"** badge appears when `updatedAt` is newer than `publishedAt`.

### Publishing a version

Open **Application → Settings** in the backoffice. The application header shows:

- The **currently published version number** and how long ago it was published (e.g., *Published v3 · 2h ago*).
- A **"Unpublished changes"** badge when the application has been edited since the last publish. This badge appears when the application's `updatedAt` timestamp is newer than its `publishedAt` timestamp.
- A **Publish** button to create a new version snapshot.

Clicking **Publish** calls `POST /api/projects/[projectId]/applications/[appId]/publish`. On success, a notification confirms the new version number. The application's `publishedVersionNumber`, `publishedVersionId`, and `publishedAt` fields are updated automatically.

### Version history panel

Click the **clock icon** in the application header to open the **Version History** panel. The panel lists all published versions for the application, sorted newest first, showing the version number and the date and time each version was published.

### API: Publishing a version

#### `POST /api/projects/[projectId]/applications/[appId]/publish`

Creates an immutable ApplicationVersion snapshot from the current application state.

**Requires:** `UPDATE` permission on the application.

**Response:** The newly created `ApplicationVersion` object, including its `versionNumber` and `id`.

**Example response:**

```json
{
  "id": "ver_abc123",
  "applicationId": "app_456",
  "versionNumber": 3,
  "publishedBy": "user_789",
  "title": { "en": "My App" },
  "createdAt": 1700000000000
}
```

#### `GET /api/projects/[projectId]/applications/[appId]/versions`

Returns the full version history for an application, sorted by `versionNumber` descending.

**Requires:** `READ` permission on the application.

**Response:** Array of `ApplicationVersion` objects.

## Custom domains

Custom domains are managed at the **project level**, not the application level. To serve an application from your own domain, go to **Project → Settings → Domains** and add a domain there.

### Version pinning for domains

By default, a domain serves the **latest published version** of its assigned application. You can optionally **pin a domain to a specific version**, so that it continues serving a known-good snapshot while you publish newer versions to other domains.

To pin a version, go to **Project → Settings → Domains**, find the domain assigned to an application, and select a version from the **version pin dropdown**. Selecting the first option (*Always latest*) removes any existing pin and resumes serving the latest published version.

Version pinning is controlled by the `targetVersionId` field on the domain record:

- When `targetVersionId` is set, the domain serves that specific `ApplicationVersion` snapshot.
- When `targetVersionId` is absent, the domain serves the latest published version.

You can also manage version pinning through the assign endpoint:

#### `POST /api/projects/[projectId]/domains/[domainId]/assign`

Assigns a target to a domain. Accepts an optional `targetVersionId` to pin the domain to a specific version.

**Request body:**

| Field | Type | Required | Description |
|---|---|---|---|
| `targetType` | `string` | Yes | The type of the target entity (e.g., `"application"`) |
| `targetId` | `string` | Yes | The ID of the target entity |
| `targetVersionId` | `string \| null` | No | Pin to a specific `ApplicationVersion` ID. Send `null` or omit to remove the pin |

**To pin to a specific version:**

```json
{
  "targetType": "application",
  "targetId": "app_456",
  "targetVersionId": "ver_abc123"
}
```

**To remove a version pin (serve latest):**

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

→ See [Domain Management](/domains/custom-domains) for details on DNS validation and domain assignment.

## Built-in authentication pages

Every application includes the following authentication pages out of the box:

| Page | Path | Purpose |
|---|---|---|
| Login | `/` (default) | User login and registration |
| Email verification | `/verify-mail` | Shown after registration — users must verify their email before gaining access |
| Forgot password | `/forgot-password` | Request a password reset link via email |
| Reset password | `/reset-password` | Set a new password using the token from the reset email |

Email verification is required for all new registrations. After signing up, users are redirected to `/verify-mail` and must click the verification link sent to their inbox.

## Registration control

By default, new users can register for your application. You can disable this under **Application → Settings → Registration** to create invite-only or closed-beta applications.

→ See [Registration Control](/applications/registration-control) for details.

## Real-time updates

Changes you make in the backoffice (adding a widget, updating a page's access rules) are reflected immediately for users without a page reload. The platform syncs application configuration via [SignalDB](/developer/local-first) in the background.

When an `ABLY_API_KEY` is configured server-side, applications use **Ably-driven real-time collection sync**: data changes are pushed to all connected clients within ~1.5 s instead of waiting for the next 5 s poll cycle. The polling interval drops to a 30 s heartbeat while Ably is active. No additional configuration is required by application authors — the feature activates automatically.

## AI-assisted creation

When the **AI features** flag is enabled for your project, the backoffice provides two AI-powered shortcuts for building applications and pages faster.

> **Prerequisite:** AI features must be enabled on the project. A project administrator can turn this on under **Project → Settings** by enabling the `aiEnabled` option. If `aiEnabled` is not set to `true`, the AI options are hidden and the API returns a `403` error if called directly.

### Creating an application with AI

When `aiEnabled` is active, the **Add Application** dialog gains an **AI** tab alongside the standard manual form.

1. Open **Settings → Applications** and click **Add Application**.
2. Switch to the **AI** tab.
3. Enter a plain-language description of the application you want to build — for example:
   > *"A small website for a bakery with a home page, an about page, and a contact form."*
4. Click **Create with AI**.

The platform sends your prompt to the AI backend, which generates a complete application including:

- An application title
- One or more routes (always including a `/` home route)
- A widget tree for each page, using widgets from the platform's widget catalog

The generated application is saved immediately and you are taken directly to its settings page, where you can review and refine the result.

**What the AI produces:**

- Routes with lowercase, hyphen-separated paths (e.g., `/about-us`, `/contact`)
- Pages built from layout widgets (such as `Container`, `WebsiteSection`, `AppShell`) and content widgets (such as `Hero`, `Text`, `Button`)
- German-language content by default, since the platform targets German-speaking users
- Navigation and footer widgets for website-style apps; `AppShell` for app-style layouts

**API:** The existing `POST /api/projects/[projectId]/applications` endpoint handles AI creation when a `prompt` field is included in the request body alongside `projectId`. If `prompt` is absent, the endpoint falls back to standard manual creation.

```json
POST /api/projects/proj_123/applications

{
  "projectId": "proj_123",
  "prompt": "A small website for a bakery with a home page, an about page, and a contact form."
}
```

### Generating a page with AI

Within an existing application, the **Add Page** dialog includes a collapsible **AI** panel (visible when `aiEnabled` is `true`).

1. Open **Settings → Applications → [your application]**.
2. Click **Add Page**.
3. Expand the **AI** section in the dialog.
4. Describe the page you want — for example:
   > *"A contact page with a heading, a short introduction paragraph, and a contact form."*
5. Click the generate button.

The AI generates a complete page — including a URL path and a widget tree — and inserts it directly into the application. You can then edit individual widgets as usual.

**API:** A dedicated endpoint handles AI page generation:

```
POST /api/projects/[projectId]/applications/[appId]/ai-generate-page
```

**Request body:**

```json
{
  "prompt": "A contact page with a heading, a short introduction paragraph, and a contact form."
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `prompt` | `string` | Yes | Plain-language description of the page to generate |

**Response:**

On success, the endpoint returns an `ApplicationContent` object containing the generated page path and its widget tree, ready to be merged into the application:

```json
{
  "path": "/contact",
  "rootWidgetId": "64f1a2b3c4d5e6f7a8b9c0d1",
  "widgets": {
    "64f1a2b3c4d5e6f7a8b9c0d1": {
      "id": "64f1a2b3c4d5e6f7a8b9c0d1",
      "name": "Container",
      "slots": {
        "content": ["64f1a2b3c4d5e6f7a8b9c0d2", "64f1a2b3c4d5e6f7a8b9c0d3"]
      }
    },
    "64f1a2b3c4d5e6f7a8b9c0d2": {
      "id": "64f1a2b3c4d5e6f7a8b9c0d2",
      "name": "Hero",
      "data": { "title": "Kontakt" }
    },
    "64f1a2b3c4d5e6f7a8b9c0d3": {
      "id": "64f1a2b3c4d5e6f7a8b9c0d3",
      "name": "Text",
      "data": { "text": { "de": "Nehmen Sie Kontakt mit uns auf." } }
    }
  }
}
```

**Error responses:**

| Status | Condition |
|---|---|
| `400 Bad Request` | The `prompt` field is missing or not a string |
| `403 Forbidden` | AI features are disabled for the project (`aiEnabled` is not `true`), or the caller does not have write permission on the project |
| `500 Internal Server Error` | The AI backend failed to generate a valid page |

**Permissions:** The caller must have write (`RLS_UPDATE`) permission on the project. Requests from users without this permission are rejected before the AI is invoked.

## API: Creating an application

### `POST /api/projects/[projectId]/applications`

Creates a new application under the given project.

**Request body:**

| Field | Type | Required | Description |
|---|---|---|---|
| `projectId` | `string` | Yes | The ID of the project to create the application in |
| `title` | `string` | Yes | The display name of the application |
| `authTenantId` | `string` | No | The authentication tenant for the application. If omitted, defaults to the project ID (app users). Pass the backoffice project ID to restrict login to employees only. |
| `prompt` | `string` | No | If provided, triggers AI-assisted application generation instead of creating a blank application |

## API: Updating application settings

Application settings are updated through a single consolidated endpoint rather than per-field sub-resource endpoints.

### `PATCH /api/projects/[projectId]/applications/[appId]`

Updates one or more user-editable fields on an application in a single request. The request body must be a JSON object containing only the fields you want to change. System-managed fields (such as `routes` and `domains`) cannot be set through this endpoint.

**Updatable fields:**

| Field | Type | Description |
|---|---|---|
| `title` | `InlineTranslation` | The display name of the application |
| `registrationMode` | `string` | Controls whether new user registration is open or closed |
| `pwaConfiguration` | `PwaConfiguration` | Progressive Web App manifest settings |
| `sitemapConfiguration` | `SitemapConfiguration` | Sitemap generation settings |
| `configuration` | `object` | Application-wide key-value configuration |
| `serverConfiguration` | `object` | Server-side key-value configuration (string values only) |
| `optIns` | `ApplicationOptIn[]` | Opt-in consent declarations shown to users |
| `compoundWidgets` | `array` | Reusable widget compositions defined on the application |

> **Note:** `authTenantId` is set at creation time and cannot be changed through the application settings UI or the `PATCH` endpoint. Choose the correct authentication tenant when creating the application.

**Example — update the application title and registration mode together:**

```json
PATCH /api/projects/proj_123/applications/app_456

{
  "title": { "en": "My App", "de": "Meine App" },
  "registrationMode": "disabled"
}
```

**Example — update only the PWA configuration:**

```json
PATCH /api/projects/proj_123/applications/app_456

{
  "pwaConfiguration": {
    "name": "My App",
    "shortName": "MyApp",
    "themeColor": "#0066cc"
  }
}
```

The endpoint validates the submitted fields against the application schema and rejects any unknown or system-managed fields with a validation error. Only fields present in the request body are modified; all other fields remain unchanged.

> **Important: full-object replacement for nested fields.** When you include a nested configuration field such as `pwaConfiguration` or `sitemapConfiguration`, the submitted object **replaces** the entire existing value — it is not deep-merged. Always send the complete configuration object, not just the properties you want to change. For example, if you only send `{ "pwaConfiguration": { "themeColor": "#ff0000" } }`, the existing `name` and `shortName` values will be removed.

> **Note:** Previous per-field endpoints such as `/title`, `/authTenantId`, `/registrationMode`, `/pwaConfiguration`, and `/sitemapConfiguration` have been removed. All application field updates must go through the consolidated `PATCH` endpoint above.

## CheckIn: Visitor registration

The **CheckIn** feature provides a public-facing visitor self-registration and management system. It is deployed as a dedicated application containing a **CheckInWidget** that handles registration, sign-in/out tracking, and GDPR-compliant data deletion.

### How it works

1. A visitor opens the public CheckIn application and fills out a registration form (name, email, company, purpose, optional contact person).
2. On submission, the widget creates a **Visitor** record and a **VisitorEvent** record (type `"in"`), then displays a success screen with the visitor's unique ID.
3. Returning visitors can enter their ID (or scan a QR code if `scannerEnabled` is configured) to load their profile, where they can sign out, sign in again, or request data deletion.

The widget tracks visitor status via `latestEventType` (`"in"` or `"out"`) and `latestEventAt` on the visitor record. Each sign-in/out action creates a new VisitorEvent and updates the visitor record.

### Data model

The feature creates two data types via template extensions:

**Visitor type** — stores visitor records:

| Field | Type | Description |
|---|---|---|
| `firstName` | `string` | First name (required) |
| `lastName` | `string` | Last name (required) |
| `email` | `string` | Email address |
| `company` | `string` | Company name |
| `purpose` | `string` | Visit purpose |
| `contactPersonId` | `string` | Foreign key to a Contact record (optional) |
| `latestEventType` | `enum["in", "out"]` | Current check-in status (system-managed) |
| `latestEventAt` | `number` | Timestamp of last event in ms (system-managed) |
| `deleteOn` | `number` | Scheduled deletion timestamp in ms (system-managed) |
| `deleteOnPreference` | `number` | Retention duration in ms (system-managed) |

**Visitor Events type** — logs sign-in/out events:

| Field | Type | Description |
|---|---|---|
| `visitorId` | `string` | Foreign key to the Visitor record (required) |
| `type` | `enum["in", "out"]` | Event type (required) |
| `createdAt` | `number` | Timestamp in ms (required) |

### API endpoints

The CheckIn widget uses the platform's **generic type data endpoints** — there are no custom visitor-specific routes. The `visitorTypeId` and `visitorEventTypeId` from the widget configuration determine which type collections are used.

| Operation | Method | Endpoint |
|---|---|---|
| Create visitor | `POST` | `/api/projects/{projectId}/types/{visitorTypeId}/data` |
| Get visitor | `GET` | `/api/projects/{projectId}/types/{visitorTypeId}/data/{visitorId}` |
| Update visitor | `PATCH` | `/api/projects/{projectId}/types/{visitorTypeId}/data/{visitorId}` |
| Delete visitor | `DELETE` | `/api/projects/{projectId}/types/{visitorTypeId}/data/{visitorId}` |
| Create event | `POST` | `/api/projects/{projectId}/types/{visitorEventTypeId}/data` |

These are the same generic endpoints used by all type data in the platform. The widget simply targets the visitor and visitor-event type collections configured in its settings. Anonymous access is allowed for the public-facing CheckIn application.

### Widget configuration

The CheckInWidget is configured with the following properties:

| Property | Type | Description |
|---|---|---|
| `title` | `InlineTranslation` | Welcome screen heading |
| `description` | `InlineTranslation` | Welcome screen description |
| `successSignInMessage` | `InlineTranslation` | Message shown after successful sign-in |
| `successSignOutMessage` | `InlineTranslation` | Message shown after successful sign-out |
| `smallPrint` | `InlineTranslation` | GDPR consent text displayed on the registration form |
| `scannerEnabled` | `boolean` | Whether to show the QR code scanner button (default: `true`) |
| `scannerButtonText` | `InlineTranslation` | Label for the scanner button |
| `deletionPeriods` | `DeletionPeriod[]` | Configurable retention periods (duration in ms, label, isDefault) |
| `visitorTypeId` | `string` | Reference to the Visitor data type |
| `visitorEventTypeId` | `string` | Reference to the VisitorEvent data type |
| `contactsTypeId` | `string` | Optional reference to a Contact data type for contact person selection |

Default deletion periods: 1 day, 7 days, 14 days, and 28 days (default).

### Notification actions

The feature template creates three webhook-triggered email actions:

| Action | Webhook | Description |
|---|---|---|
| Send Visitor Pass | `checkin-visitor-pass` | Sends visitor details and QR code to visitor's email |
| Notify Contact Person | `checkin-contact-notification` | Notifies the linked contact person of a visitor sign-in |
| Send Calendar Invitation | `checkin-calendar-invite` | Sends an iCalendar (RFC 5545) file as email attachment |

### GDPR automatic deletion

A cron action runs daily at 2:00 AM UTC to delete expired visitor records (where `deleteOn <= now`). Visitors select their preferred retention period during registration.

### Backoffice management

Two backoffice pages are created by the template:

- **Visitors** — table view with search, filter, and manual sign-in/out. Duplicate detection enabled (80% threshold on name + email).
- **Visitor Events** — timeline of all sign-in/out events, linked to visitor records.

## Related

- [Registration Control](/applications/registration-control)
- [User Types](/applications/user-types)
- [Default Dashboard](/applications/default-dashboard)
- [Widget System](/developer/widget-system) — developer guide for building custom widgets
- [Badges](/users/badges) — achievements awarded for platform milestones
