Skip to content

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.

label: {
    locales: {
        en_US: 'My App',
        sr: 'Moja Aplikacija',
        de_DE: 'Meine App',
    }
}

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.

views: {
    main: path.resolve(__dirname, 'src/main.tsx'),
}

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.

distPath: path.resolve(__dirname, "dist"),

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.

ssrRenderer: path.resolve(__dirname, 'src/ssr/renderer.tsx'),

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: [
    "reviewer",
    "documents",
    "documents:read",
    "documents:write",
    "documents:delete",
]

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.

install_permissions: [
    "manage base_components",
    "manage integrations",
]

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.

runtime_permissions: [
    "documents:read",
    "documents:write",
]

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: [
    "documents:delete",
    "documents:archive",
]

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.