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:
- The
ptkltoolkit installed - An active profile pointing at your target environment:
See Profiles for details.
Starting the Dev Server
Run ptkl forge dev from your app directory to start local development:
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
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:
- Watch-builds your service scripts and holds the bundled code in memory
- When a request arrives, sends the bundled code along with the request payload to the platform's Forge Runtime sandbox
- The sandbox executes the code with the same globals (
input,query,headers,context,response,axiosAdapter) available in production - 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:
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
- App Manifest —
ptkl.config.jsconfiguration reference - Services — service execution model and scripting
- Lifecycle Scripts — install and uninstall scripts
ptkl forge— CLI command reference- Profiles — managing environment profiles