Applications

How applications, pages, widgets, and routing work in the Business Platform

applicationswidgetsrouting

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

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:

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:

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 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:

{
  "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:

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

To remove a version pin (serve latest):

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

→ See Domain Management 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 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 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.

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:

{
  "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:

{
  "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:

PATCH /api/projects/proj_123/applications/app_456

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

Example — update only the PWA configuration:

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.