Skip to content

Local Development

How to develop and test Forge apps locally using the ptkl toolkit — including views, services, and public views.


Prerequisites

Before starting local development, make sure you have:

  1. The ptkl toolkit installed
  2. An active profile pointing at your target environment:
ptkl profile new \
  --name dev \
  --username you@example.com \
  --password \
  --project my-project

See Profiles for details.


Starting the Dev Server

Run ptkl forge dev from your app directory to start local development:

ptkl forge dev -p ./my-app

This starts up to three things depending on your app's configuration:

Component Port What it does
Vite dev server 5173 (default) Serves your app's view with hot module replacement
Service dev server 4113 Executes services locally (only if the app declares services)
Dev hub 4111 Relays embeddable view bundles between apps (only if the app has embeddable platform views)

Open the Vite URL shown in your terminal (e.g. http://localhost:5173) to see your app.

Options

Option Description
-p, --path <path> Path to the app directory (required)
--view <view> Which view to serve (defaults to main)
-m, --mode <mode> Vite mode — controls which .env.[mode] file is loaded (defaults to development)
--env <env> Target environment: dev or live (defaults to dev)

Testing Services Locally

When your app declares services, ptkl forge dev automatically starts a service dev server on port 4113. Services are watch-built — every time you change a service file, it is rebuilt automatically.

Calling a Service

Send requests to http://localhost:4113/{service-name}:

curl -X POST http://localhost:4113/track-invoice \
  -H "Content-Type: application/json" \
  -d '{"tracking": "INV-2024-001"}'

Listing Available Services

curl http://localhost:4113/

Returns a JSON list of all services declared in your ptkl.config.js with their name, access scope, and permissions.

How It Works

The service dev server does not execute code locally. Instead, it:

  1. Watch-builds your service scripts and holds the bundled code in memory
  2. When a request arrives, sends the bundled code along with the request payload to the platform's Forge Runtime sandbox
  3. The sandbox executes the code with the same globals (input, query, headers, context, response, axiosAdapter) available in production
  4. Returns the response to your terminal

This means service behavior in development matches production — the same sandbox, same permissions, same platform API access.

Service Dev Server Response

The service dev server response wraps the service output with execution logs:

{
  "response": {
    "statusCode": 200,
    "contentType": "application/json",
    "body": { "tracking_number": "INV-2024-001", "status": "paid" }
  },
  "logs": ["Fetched invoice successfully"]
}

Example: Platform App with Services

// ptkl.config.js
export default {
    name: 'invoice-app',
    version: '1.0.0',
    type: 'platform',

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

    services: [
        {
            name: 'track-invoice',
            script: path.resolve(__dirname, 'src/services/trackInvoice.ts'),
            access: 'public',
            permissions: ['invoices:read'],
        },
        {
            name: 'create-invoice',
            script: path.resolve(__dirname, 'src/services/createInvoice.ts'),
            access: 'platform',
            permissions: ['invoices:read', 'invoices:write'],
        },
    ],

    runtime_permissions: ['invoices:read', 'invoices:write'],
}
# Start dev server — Vite on :5173, services on :4113
ptkl forge dev -p .

# In another terminal:
# List services
curl http://localhost:4113/

# Call the public service
curl -X POST http://localhost:4113/track-invoice \
  -H "Content-Type: application/json" \
  -d '{"tracking": "INV-2024-001"}'

# Call the platform service
curl -X POST http://localhost:4113/create-invoice \
  -H "Content-Type: application/json" \
  -d '{"customer_id": "cust_1", "items": [{"name": "Widget", "price": 10, "quantity": 2}]}'

Example: Service App (Headless)

A type: "service" app has no views — only services:

// ptkl.config.js
export default {
    name: 'email-sender',
    version: '1.0.0',
    type: 'service',

    services: [
        {
            name: 'send-email',
            script: path.resolve(__dirname, 'src/services/sendEmail.ts'),
            permissions: ['mail:send'],
        },
    ],

    runtime_permissions: ['mail:send'],
}
# Only the service dev server starts (no Vite, no views)
ptkl forge dev -p .

# Test it
curl -X POST http://localhost:4113/send-email \
  -H "Content-Type: application/json" \
  -d '{"to": "user@example.com", "subject": "Hello", "body": "Test"}'

Testing Public Views Locally

Public views are standalone single-page applications served without the platform shell — no sidebar, no header, no $forge global. In production, they are accessed at {uuid}.public.ptkl.app/{view-name}.

To develop and test a public view locally, use the --view option:

ptkl forge dev -p ./my-app --view public-invoice

This starts the Vite dev server with the specified public view's entry point loaded. Open the URL shown in your terminal (e.g. http://localhost:5173) to see the view.

How Public Views Differ in Dev Mode

Platform view (main) Public view
Shell Runs inside platform shell with sidebar, header Standalone — no shell, no $forge
Dev Hub Pushed to dev hub for cross-app embedding Not pushed to dev hub
API calls Uses platform API via injected env vars Uses platform API via injected env vars
Browser URL http://localhost:5173 http://localhost:5173

Tip

Since public views don't have $forge, you cannot use $forge.requestPermissions() or other Forge APIs inside them. If your public view needs to call the platform API, use the injected VITE_API_HOST environment variable or call your app's services.

Calling Services from a Public View

A common pattern is to have a public view call one of your app's services. In dev mode, point your fetch calls at http://localhost:4113:

// src/publicInvoice.tsx
const API_BASE = import.meta.env.DEV
    ? 'http://localhost:4113'
    : '';  // in production, use relative URL or service domain

const response = await fetch(`${API_BASE}/track-invoice`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ tracking: trackingNumber }),
});

In production, public views have a built-in API proxy at /{view-name}/api/* that handles authentication automatically. During development, call the service dev server directly.

Example: Platform App with a Public View

// ptkl.config.js
export default {
    name: 'invoice-app',
    version: '1.0.0',
    type: 'platform',

    views: {
        main: path.resolve(__dirname, 'src/main.tsx'),
        'public-invoice': {
            entry: path.resolve(__dirname, 'src/publicInvoice.tsx'),
            access: 'public',
            permissions: ['invoices:read'],
        },
    },

    services: [
        {
            name: 'track-invoice',
            script: path.resolve(__dirname, 'src/services/trackInvoice.ts'),
            access: 'public',
            permissions: ['invoices:read'],
        },
    ],

    runtime_permissions: ['invoices:read'],
}

Development workflow:

# Terminal 1: develop the main platform view
ptkl forge dev -p .

# Terminal 2: develop the public view
ptkl forge dev -p . --view public-invoice

Note

Running two ptkl forge dev instances for the same app serves different views on different ports. The service dev server (port 4113) is shared — both views can call the same services.


Production URLs

Once deployed with ptkl forge bundle --upload, your app's resources are available at:

Views

Type Domain Example
Platform view Loaded inside the platform dashboard
Public view (live) {uuid}.public.ptkl.app/{view} abc-123.public.ptkl.app/public-invoice
Public view (dev) {uuid}.stage.public.ptkl.app/{view} abc-123.stage.public.ptkl.app/public-invoice

Services

Access Domain Example
Public (live) {uuid}.public.service.ptkl.app/{service} abc-123.public.service.ptkl.app/track-invoice
Public (dev) {uuid}.stage.public.service.ptkl.app/{service} abc-123.stage.public.service.ptkl.app/track-invoice
Platform (live) {uuid}.service.ptkl.app/{service} abc-123.service.ptkl.app/create-invoice
Platform (dev) {uuid}.stage.service.ptkl.app/{service} abc-123.stage.service.ptkl.app/create-invoice

Your app's UUID is assigned when the app is first created and is visible in the platform UI.


See Also