Skip to content

Function Manifest

The function manifest declares per-function configuration for the Thunder runtime. It controls the deploy flavor, routing, environment access, network allowlists, and resource limits.

  • Schema: schemas/function-manifest.v2.schema.json
  • JSON Schema draft: 2020-12

The runtime accepts only manifestVersion: 2. Payloads with manifestVersion: 1 are rejected.

{
"manifestVersion": 2
}

FlavorDescription
singleSingle-entrypoint function. Must not define routes.
routed-appMulti-route application. Must define at least one route.

FieldTypeDescription
manifestVersion2 (integer)Schema version (must be 2)
namestringFunction name/slug
entrypointstringEntry JS/TS file path
flavor"single" or "routed-app"Deploy flavor
networkobjectNetwork access policy (required)
FieldTypeDescription
routesarrayRoute table (required for routed-app, forbidden for single)
envobjectEnvironment variable access controls
resourcesobjectIsolate resource limits
authobjectAuthentication preferences
observabilityobjectLogging and tracing preferences
profilesobjectEnvironment-specific overrides

Controls which environment variables the function can access.

FieldTypeDescription
allowstring[]List of environment variable names the function can read
secretRefsstring[]Secret variable names resolved at runtime
{
"env": {
"allow": ["LOG_LEVEL", "API_BASE_URL"],
"secretRefs": ["APP_SECRET", "DATABASE_URL"]
}
}

Network access policy enforced per function.

FieldTypeDescription
mode"allowlist"Network mode (only allowlist is supported)
allowstring[]List of allowed targets (host:port format). Minimum 1 item
{
"network": {
"mode": "allowlist",
"allow": [
"api.example.com:443",
"db.internal.corp:5432"
]
}
}

Wildcard (*) is not allowed in network.allow. Targets that collide with SSRF deny ranges are rejected at deploy time.

The runtime blocks the following ranges regardless of manifest configuration:

RangeDescription
127.0.0.0/8Loopback
10.0.0.0/8RFC 1918 private
172.16.0.0/12RFC 1918 private
192.168.0.0/16RFC 1918 private
169.254.0.0/16Link-local / cloud metadata
0.0.0.0/8Reserved
::1/128IPv6 loopback
fc00::/7IPv6 unique local
fe80::/10IPv6 link-local

Per-function isolate resource limits. These override the runtime defaults when set.

FieldTypeDefaultDescription
maxHeapMiBinteger128Per-isolate heap limit in MiB. 0 = unlimited
cpuTimeMsinteger50000Per-request CPU time limit in ms. 0 = unlimited
wallClockTimeoutMsinteger60000Per-request wall clock timeout in ms. 0 = unlimited
vfsTotalQuotaBytesinteger10485760Writable VFS quota per isolate (10 MiB)
vfsMaxFileBytesinteger5242880Max writable file size in VFS (5 MiB)
egressMaxRequestsPerExecutionintegerMax outbound requests per execution
{
"resources": {
"maxHeapMiB": 256,
"cpuTimeMs": 30000,
"wallClockTimeoutMs": 45000,
"vfsTotalQuotaBytes": 20971520,
"vfsMaxFileBytes": 10485760
}
}

Route table for routed-app flavor. Each route is either a function route or an asset route.

FieldTypeRequiredDescription
kind"function"yesRoute type
pathstringyesURL path prefix (must start with /)
entrypointstringyesJS/TS entrypoint file
methodsstring[]noAllowed HTTP methods
FieldTypeRequiredDescription
kind"asset"yesRoute type
pathstringyesURL path prefix (must start with /)
assetDirstringyesDirectory containing static files
methodsstring[]noOnly GET and HEAD are allowed
{
"routes": [
{
"kind": "function",
"path": "/api",
"entrypoint": "./api/index.ts",
"methods": ["GET", "POST"]
},
{
"kind": "asset",
"path": "/static",
"assetDir": "./public"
}
]
}
  • single flavor must not define routes.
  • routed-app flavor must define at least one route.
  • Function routes require a non-empty entrypoint and cannot define assetDir.
  • Asset routes require a non-empty assetDir and cannot define entrypoint.
  • Asset route methods, if provided, can only contain GET and HEAD.
  • Routes are validated for path/method collisions at bundle time. Ambiguous overlaps fail the build.

FieldTypeDescription
verifyJwtbooleanWhether to verify JWT tokens
{
"auth": {
"verifyJwt": true
}
}

FieldTypeDescription
logLevelstringLog level for the function
traceSamplePercentintegerTrace sampling percentage (0-100)
{
"observability": {
"logLevel": "info",
"traceSamplePercent": 10
}
}

Profiles provide environment-specific overrides. Profile names must match ^[a-z][a-z0-9_-]{0,31}$.

Each profile can override: env, network, resources, auth, and observability.

{
"profiles": {
"staging": {
"env": {
"allow": ["LOG_LEVEL", "STAGING_API_URL"],
"secretRefs": ["STAGING_SECRET"]
},
"network": {
"allow": ["staging-api.example.com:443"]
},
"resources": {
"maxHeapMiB": 64,
"cpuTimeMs": 10000
}
},
"production": {
"env": {
"allow": ["LOG_LEVEL"],
"secretRefs": ["PROD_SECRET", "DATABASE_URL"]
},
"network": {
"allow": ["api.example.com:443", "db.prod.internal:5432"]
},
"resources": {
"maxHeapMiB": 256,
"cpuTimeMs": 50000
}
}
}
}

Select a profile at deploy time with the x-function-manifest-profile header.


When deploying a function via POST /_internal/functions, the manifest is provided as a Base64-encoded JSON string in the x-function-manifest-b64 header.

Terminal window
MANIFEST_B64=$(base64 < function.manifest.json)
curl -X POST \
-H "X-API-Key: $KEY" \
-H "x-function-name: hello" \
-H "x-function-manifest-b64: $MANIFEST_B64" \
--data-binary @hello.eszip \
http://localhost:9000/_internal/functions

To select a profile:

Terminal window
curl -X POST \
-H "X-API-Key: $KEY" \
-H "x-function-name: hello" \
-H "x-function-manifest-b64: $MANIFEST_B64" \
-H "x-function-manifest-profile: production" \
--data-binary @hello.eszip \
http://localhost:9000/_internal/functions

If the header is absent but the uploaded bundle contains an embedded manifest (from thunder bundle --manifest), the runtime resolves and applies the embedded manifest.

  1. JSON decode / parse
  2. JSON Schema validation (v2, draft 2020-12)
  3. Semantic checks (flavor/routes rules, SSRF denylist)

If any step fails, the response is 400 Bad Request.

  • PUT /_internal/functions/{name}: replaces the manifest if x-function-manifest-b64 is provided; preserves the existing manifest if omitted.
  • POST /_internal/functions/{name}/reload: preserves the currently attached manifest.
Terminal window
curl -H "X-API-Key: $KEY" \
http://localhost:9000/_internal/functions/hello/manifest

Returns 200 with the resolved manifest, or 404 if no manifest is attached.


{
"$schema": "https://thunder.dev/schemas/function-manifest.v2.schema.json",
"manifestVersion": 2,
"name": "hello",
"entrypoint": "./index.ts",
"flavor": "single",
"env": {
"allow": ["LOG_LEVEL"],
"secretRefs": ["APP_SECRET"]
},
"network": {
"mode": "allowlist",
"allow": ["api.example.com:443"]
},
"resources": {
"maxHeapMiB": 128,
"cpuTimeMs": 50000,
"wallClockTimeoutMs": 60000
}
}
{
"$schema": "https://thunder.dev/schemas/function-manifest.v2.schema.json",
"manifestVersion": 2,
"name": "my-app",
"entrypoint": "./app/main.ts",
"flavor": "routed-app",
"network": {
"mode": "allowlist",
"allow": ["api.example.com:443", "cdn.example.com:443"]
},
"routes": [
{
"kind": "function",
"path": "/api/users",
"entrypoint": "./app/routes/users.ts",
"methods": ["GET", "POST"]
},
{
"kind": "function",
"path": "/api/health",
"entrypoint": "./app/routes/health.ts",
"methods": ["GET"]
},
{
"kind": "asset",
"path": "/static",
"assetDir": "./public"
}
],
"env": {
"allow": ["NODE_ENV"],
"secretRefs": ["DATABASE_URL"]
},
"resources": {
"maxHeapMiB": 256,
"cpuTimeMs": 30000,
"wallClockTimeoutMs": 45000
},
"profiles": {
"staging": {
"network": {
"allow": ["staging-api.example.com:443"]
},
"resources": {
"maxHeapMiB": 64
}
}
}
}

thunder watch deploys functions without attaching a manifest. To use manifest-enforced behavior during development, deploy via the admin endpoint with the x-function-manifest-b64 header.