App Manifest
Every Forge app is defined by a ptkl.config.js file — a default export that describes the app's identity, entry points, lifecycle scripts, services, and permissions.
Example
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default {
name: 'invoice-app',
version: '1.0.0',
icon: 'icon.svg',
type: 'platform',
label: {
locales: {
en_US: 'Invoice App',
sr: 'Aplikacija za fakture',
}
},
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
'public-invoice': {
entry: path.resolve(__dirname, 'src/publicInvoice.tsx'),
access: 'public',
permissions: ['invoices:read', 'documents:get'],
},
},
services: [
{
name: 'track-invoice',
script: path.resolve(__dirname, 'src/services/trackInvoice.ts'),
access: 'public',
permissions: ['invoices:read'],
label: { locales: { en_US: 'Track Invoice' } },
description: { locales: { en_US: 'Look up invoice status by tracking number' } },
},
{
name: 'create-invoice',
script: path.resolve(__dirname, 'src/services/createInvoice.ts'),
access: 'platform',
permissions: ['invoices:read', 'invoices:write'],
},
],
scripts: {
install: path.resolve(__dirname, 'src/scripts/install.ts'),
uninstall: path.resolve(__dirname, 'src/scripts/uninstall.ts'),
},
distPath: path.resolve(__dirname, "dist"),
permissions: [
"invoices",
"invoices:read",
"invoices:write",
],
install_permissions: [
"manage base_components",
],
runtime_permissions: [
"invoices:read",
"invoices:write",
"documents:get",
],
}
Fields
name
| Type | string |
| Required | Yes |
Unique identifier for the app. Used as the storage key and to match the app record when uploading new versions.
version
| Type | string |
| Required | Yes |
Semantic version string (e.g. "1.0.0"). Each uploaded version is stored separately — an app can have up to 5 versions.
icon
| Type | string |
| Required | No |
Path or filename for the app icon. Used as the favicon when the app is loaded in the platform.
type
| Type | "platform" | "public" | "service" |
| Required | No |
| Default | "platform" |
Determines the app's visibility, infrastructure provisioning, and what resources it can expose.
| Value | Description |
|---|---|
platform |
Internal app. Views default to access: "platform" but individual views can be set to "public". Can declare services with either access scope. |
public |
Public-facing app. All views and services must be access: "public". Automatically receives an API role, API user, API token, and a generated subdomain (e.g. happy-cat.ptkl.app). Enables ssrRenderer. |
service |
Headless API-only app. Cannot declare views. All services must be access: "public". Accessed exclusively through service domains. |
label
| Type | { locales: Record<string, string> } |
| Required | No |
Internationalized display name shown in the platform UI and used as document.title when the app loads.
views
| Type | Record<string, string \| ViewDefinition> |
| Required | Yes (except for type: "service") |
Map of view names to their entry points. Each value can be a simple file path string or an object with access control.
When the value is a string, it is treated as the entry point path. The view inherits its access scope from the app type: "platform" for platform apps, "public" for public apps.
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
'public-invoice': {
entry: path.resolve(__dirname, 'src/publicInvoice.tsx'),
access: 'public',
permissions: ['invoices:read', 'documents:get'],
},
}
Object entries accept:
| Field | Type | Description |
|---|---|---|
entry |
string |
Path to the view's entry point file. Required. |
access |
"platform" | "public" |
Access scope. Defaults to the app type's default. |
permissions |
string[] |
Subset of runtime_permissions this view needs. Used to mint a scoped token for the view's API proxy. |
Both formats can be mixed in the same views object. The main view is the primary entry point loaded by the platform shell.
Public views are standalone SPAs
Views with access: "public" are served as standalone single-page applications without the platform shell (no sidebar, no header, no $forge global). They are accessed through {uuid}.public.ptkl.app/{view-name}.
Access scope constraints
type: "public"apps — all views must beaccess: "public"type: "service"apps — views are not allowedtype: "platform"apps — views can be either"platform"or"public"
Limits: Maximum 10 views per app. View names must match ^[a-z][a-z0-9-]{0,62}[a-z0-9]$ (lowercase, hyphens allowed, 2–64 characters). The name main is also allowed.
services
| Type | ServiceDefinition[] |
| Required | No |
Array of service definitions. Services are server-side scripts that execute in an isolated sandbox and are callable via HTTP. See Services for the full execution model.
services: [
{
name: 'track-invoice',
script: path.resolve(__dirname, 'src/services/trackInvoice.ts'),
access: 'public',
permissions: ['invoices:read'],
label: { locales: { en_US: 'Track Invoice' } },
description: { locales: { en_US: 'Look up invoice status by tracking number' } },
},
]
Each service definition has:
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
Yes | Unique name for the service. Appears in the URL path. Must match ^[a-z][a-z0-9-]{1,62}[a-z0-9]$. |
script |
string |
Yes | Path to the service script source file. |
access |
"platform" | "public" |
No | Access scope. Defaults to "public" for public and service apps. |
permissions |
string[] |
No | Subset of runtime_permissions the service needs. A scoped token is minted with these permissions. |
label |
{ locales: Record<string, string> } |
No | Display name for the service. |
description |
{ locales: Record<string, string> } |
No | Human-readable description of what the service does. |
Access scope constraints
type: "public"apps — all services must beaccess: "public"type: "service"apps — services can be either"platform"or"public"type: "platform"apps — services can be either"platform"or"public"
Limits: Maximum 20 services per app. Service permissions must be a subset of the app's runtime_permissions.
scripts
| Type | { install?: string, uninstall?: string } |
| Required | No |
Paths to lifecycle scripts that run during deployment and removal. See Lifecycle Scripts for details.
scripts: {
install: path.resolve(__dirname, 'src/scripts/install.ts'),
uninstall: path.resolve(__dirname, 'src/scripts/uninstall.ts'),
}
install— Runs every time a version is deployed. Must be idempotent.uninstall— Runs when a version is revoked or the app is deleted.
distPath
| Type | string |
| Required | No |
Absolute path to the build output directory. This is where the bundled assets (main.bundle.js, main.css, etc.) are written during the build process.
ssrRenderer
| Type | string |
| Required | No |
| Available | Only when type is "public" |
Path to the server-side rendering entry point. When provided, the platform uses this file to pre-render the app on the server before sending it to the client. This option is only available for public apps.
permissions
| Type | string[] |
| Required | No |
List of permissions that the app exposes to the platform's permission system. Once deployed, these permissions become available to assign to any role on the platform — allowing fine-grained access control scoped to the app's functionality.
Permissions are additive
The app declares what permissions exist. Platform administrators then assign these permissions to roles through the standard role management interface. Users with those roles will have the corresponding access when using the app.
install_permissions
| Type | string[] |
| Required | No |
List of platform permissions the install and uninstall scripts are allowed to use. Before a user installs the app, they are shown a consent screen listing exactly these permissions — similar to an OAuth scope screen. The script runs with a token scoped to only these permissions and cannot act outside of them.
If omitted, the install script still executes but receives a token with no permissions. Any call to a Protokol API that requires authorization will fail — the script can only perform work that does not depend on platform API access.
runtime_permissions
| Type | string[] |
| Required | No |
List of platform permissions the app is allowed to use at runtime. This is the permission pool for three consumers:
- Platform views — When a user launches the app, the runtime token contains the intersection of
runtime_permissionsand the user's own permissions. - Public views — The view's
permissionsarray (a subset ofruntime_permissions) determines the scoped token. No user context is involved. - Services — Same as public views: the service's
permissionsarray is used to mint a scoped token.
If omitted, the app's UI and services receive no platform permissions at runtime.
entitlements
| Type | string[] |
| Required | No |
List of platform permissions that are unconditionally added to the app's runtime token, regardless of the launching user's own roles. Unlike runtime_permissions, entitlements bypass the user-intersection step — the app always receives these permissions when it starts.
Use entitlements for capabilities the app always needs to function correctly, independent of who is using it.
Entitlements bypass user permission checks
Because entitlements are merged unconditionally, they should only be used for permissions the app genuinely requires to operate — not to grant elevated access to the end user.
Combined limit
The total count of runtime_permissions and entitlements combined must not exceed 100 entries. This limit is enforced during bundle upload and installation.
Bundle Output
After building, the app is packaged as a .tar.gz bundle with the following structure:
invoice-app-1.0.0.tar.gz
├── manifest.json # Generated metadata
├── main.bundle.js # Platform view bundle
├── main.css
├── public-invoice.bundle.js # Public view bundle
├── public-invoice.css
├── scripts/
│ ├── install.js
│ └── uninstall.js
└── services/
├── track-invoice.js
└── create-invoice.js
The manifest.json in the bundle contains the serialized metadata — name, version, label, icon, permissions, views with resolved access scopes, services, and script paths.
manifest.json Format
{
"name": "invoice-app",
"version": "1.0.0",
"type": "platform",
"label": { "locales": { "en_US": "Invoice App" } },
"icon": "data:image/svg+xml;base64,...",
"permissions": ["invoices", "invoices:read", "invoices:write"],
"install_permissions": ["manage base_components"],
"runtime_permissions": ["invoices:read", "invoices:write", "documents:get"],
"entitlements": [],
"views": {
"main": "main.bundle.js",
"public-invoice": {
"entry": "public-invoice.bundle.js",
"access": "public",
"permissions": ["invoices:read", "documents:get"]
}
},
"services": [
{
"name": "track-invoice",
"script": "services/track-invoice.js",
"access": "public",
"permissions": ["invoices:read"],
"label": { "locales": { "en_US": "Track Invoice" } },
"description": { "locales": { "en_US": "Look up invoice status by tracking number" } }
},
{
"name": "create-invoice",
"script": "services/create-invoice.js",
"access": "platform",
"permissions": ["invoices:read", "invoices:write"]
}
],
"scripts": {
"install": "install.script.js",
"uninstall": "uninstall.script.js"
}
}
Views in the manifest use two formats:
- String (
"main.bundle.js") — no explicit access scope; inherits from app type. - Object (
{ entry, access, permissions }) — view with explicit access scope and optional scoped permissions.
App Types at a Glance
| Feature | platform |
public |
service |
|---|---|---|---|
| Views | Yes (platform or public) | Yes (public only) | No |
| Services | Yes (platform or public) | Yes (public only) | Yes (platform or public) |
| Platform shell (sidebar, header) | Yes | No | N/A |
| Custom domains | No | Yes | No |
| SSR | No | Yes | N/A |
| Auto-provisioned API user | No | Yes | No |
| Lifecycle scripts | Yes | Yes | Yes |
| UUID-based service domain | When services declared | When services declared | Yes |
Examples by Type
An internal app with views inside the platform shell and services with mixed access scopes.
export default {
name: 'invoice-app',
version: '1.0.0',
type: 'platform',
label: { locales: { en_US: 'Invoice App' } },
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
},
services: [
{
name: 'create-invoice',
script: path.resolve(__dirname, 'src/services/createInvoice.ts'),
access: 'platform',
permissions: ['invoices:read', 'invoices:write'],
},
],
runtime_permissions: ['invoices:read', 'invoices:write'],
}
A platform app that also exposes public views and services. Public views are standalone SPAs served outside the platform shell at {uuid}.public.ptkl.app/{view-name}. This is useful when a platform app needs both an internal UI for operators and a public-facing page for end users.
export default {
name: 'invoice-app',
version: '1.0.0',
type: 'platform',
label: { locales: { en_US: 'Invoice App' } },
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
'public-invoice': {
entry: path.resolve(__dirname, 'src/publicInvoice.tsx'),
access: 'public',
permissions: ['invoices:read'],
},
},
services: [
{
name: 'create-invoice',
script: path.resolve(__dirname, 'src/services/createInvoice.ts'),
access: 'platform',
permissions: ['invoices:read', 'invoices:write'],
},
{
name: 'track-invoice',
script: path.resolve(__dirname, 'src/services/trackInvoice.ts'),
access: 'public',
permissions: ['invoices:read'],
},
],
runtime_permissions: ['invoices:read', 'invoices:write'],
}
A headless API-only app — no views, no platform shell. All services must be access: "public".
export default {
name: 'email-sender',
version: '1.0.0',
type: 'service',
label: { locales: { en_US: 'Email Sender' } },
services: [
{
name: 'send-email',
script: path.resolve(__dirname, 'src/services/sendEmail.ts'),
permissions: ['mail:send'],
},
{
name: 'send-bulk',
script: path.resolve(__dirname, 'src/services/sendBulk.ts'),
permissions: ['mail:send', 'contacts:read'],
},
],
scripts: {
install: path.resolve(__dirname, 'src/scripts/install.ts'),
},
install_permissions: ['manage integrations'],
runtime_permissions: ['mail:send', 'contacts:read'],
}
A public-facing app with its own subdomain. All views and services must be access: "public".
export default {
name: 'storefront',
version: '1.0.0',
type: 'public',
label: { locales: { en_US: 'Storefront' } },
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
},
services: [
{
name: 'add-to-cart',
script: path.resolve(__dirname, 'src/services/addToCart.ts'),
permissions: ['orders:write'],
},
],
ssrRenderer: path.resolve(__dirname, 'src/ssr/renderer.tsx'),
runtime_permissions: ['orders:write', 'products:read'],
}
See Also
- Services — service execution model, globals, and endpoints
- Lifecycle Scripts — install and uninstall scripts
- AppView — Embedding Views — embedding views from other apps
- Custom Domains — domain setup for public apps