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, 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: 'operator',
version: '0.3.0',
icon: 'react.svg',
type: 'public',
label: {
locales: {
en_US: 'Operator',
sr: 'Operator',
}
},
views: {
main: path.resolve(__dirname, 'src/main.tsx'),
},
scripts: {
install: path.resolve(__dirname, 'src/scripts/install.ts'),
uninstall: path.resolve(__dirname, 'src/scripts/uninstall.ts'),
},
distPath: path.resolve(__dirname, "dist"),
ssrRenderer: path.resolve(__dirname, 'src/ssr/renderer.tsx'),
permissions: [
"reviewer",
"documents",
"documents:read",
"documents:write",
"documents:delete",
],
install_permissions: [
"manage base_components",
"manage integrations",
],
runtime_permissions: [
"documents:read",
"documents:write",
],
entitlements: [
"documents:delete",
"documents:archive",
],
}
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. "0.3.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" |
| Required | No |
| Default | "platform" |
Determines the app's visibility and how the platform provisions its infrastructure.
| Value | Description |
|---|---|
platform |
Internal app. No auto-provisioned API user, domain, or SSR support. |
public |
Public-facing app. Automatically receives an API role, API user, API token, and a generated subdomain (e.g. happy-cat.ptkl.app). Enables ssrRenderer. |
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> |
| Required | Yes |
Map of view names to their entry point file paths. The main view is required — it is the primary entry point that gets bundled as main.bundle.js and loaded by the platform. Additional views will be supported in future releases.
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's UI is allowed to use at runtime. When a user launches the app, it receives a scoped token that is limited to the intersection of what is declared here and the permissions the launching user actually holds. The app can never act beyond what the current user is allowed to do.
If a user only has documents:read, the token will only contain documents:read even if the app declared both documents:read and documents:write. The app should handle this gracefully by checking what the token contains and hiding or disabling functionality the user cannot access.
If omitted, the app's UI receives 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. Typical examples include service-level read access, event publishing permissions, or internal API scopes that are not tied to individual user roles.
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. An entitlement allows the app to call an API, but the platform's resource-level access control still governs what data is actually accessible.
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:
my-app/
├── manifest.json # Generated from the manifest — name, version, label, icon, permissions, scripts
├── main.bundle.js # Compiled main view entry point (ES module)
├── main.css # Compiled styles (if any)
└── scripts/ # Compiled lifecycle scripts (if defined)
├── install.js
└── uninstall.js
The manifest.json in the bundle contains the serialized metadata (name, version, label, icon, permissions, script paths) while the build artifacts are the compiled output from the source paths defined in the manifest.