Title: Applications Description: How applications, pages, widgets, and routing work in the Business Platform Tags: applications, widgets, routing --- 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/, 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 (HONOHANDLEDPATHS) 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: Calls ctx.getCurrentUser() to resolve the current user from the session. Returns 401 Unauthorized if no user is found. 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 PUBLICBUGFENDERAPI_KEY, you must update your environment configuration as described below. Required environment variable | Variable | Description | |---|---| | PUBLICBUGSINKDSN | 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: PUBLICBUGSINKDSN=https://@bugsink.example.com/ When PUBLICBUGSINKDSN 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 PUBLICBUGSINKDSN 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 PUBLICPLAUSIBLESRC 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 | |---|---|---|---| | PUBLICPLAUSIBLESRC | 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 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 PUBLICPLAUSIBLESRC to your instance's script URL and configure plausibleDomain in application settings: PUBLICPLAUSIBLESRC=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 ABLYAPIKEY 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. Open Settings → Applications and click Add Application. Switch to the AI tab. 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." 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). Open Settings → Applications → [your application]. Click Add Page. Expand the AI section in the dialog. Describe the page you want — for example: > "A contact page with a heading, a short introduction paragraph, and a contact form." 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/proj123/applications/app456 { "title": { "en": "My App", "de": "Meine App" }, "registrationMode": "disabled" } Example — update only the PWA configuration: PATCH /api/projects/proj123/applications/app456 { "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 A visitor opens the public CheckIn application and fills out a registration form (name, email, company, purpose, optional contact person). 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. 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 User Types Default Dashboard Widget System — developer guide for building custom widgets Badges — achievements awarded for platform milestones