Skip to content

Photon Deployment Guide

Production deployment strategies for photon applications.


Table of Contents


Overview

Photons can be deployed in multiple ways depending on your needs:

TargetBest ForScaling
Standalone BinaryZero-dependency distribution, air-gapped envsSingle binary per platform
DockerSelf-hosted, full controlHorizontal with orchestrator
Cloudflare WorkersEdge computing, global low latencyAutomatic
AWS LambdaServerless, pay-per-useAutomatic
SystemdTraditional VPS, always-on servicesManual/VM autoscaling

Standalone Binary

Compile any photon into a self-contained executable. The target machine needs no Node.js, no npm, no Photon runtime — just the binary.

Build

bash
photon build my-tool                    # Binary for current platform
photon build my-tool -o my-tool-bin     # Custom output name
photon build my-tool -t bun-linux-x64   # Cross-compile for Linux x64
photon build my-tool --with-app         # Embed Beam UI for desktop app mode

What Gets Bundled

  • The photon source and all @dependencies
  • Transitive @photon dependencies (resolved recursively)
  • The embedded Photon runtime
  • Beam frontend assets (with --with-app)

Cross-Compilation Targets

TargetPlatform
bun-darwin-arm64macOS Apple Silicon
bun-darwin-x64macOS Intel
bun-linux-x64Linux x64
bun-linux-arm64Linux ARM64

Limitations

  • @mcp dependencies (external MCP servers) cannot be bundled — a warning is emitted
  • @cli dependencies (system binaries like ffmpeg) must be present on the target machine
  • Requires Bun installed on the build machine

Distribution

The resulting binary is fully portable:

bash
# Build on macOS, deploy to Linux server
photon build my-tool -t bun-linux-x64
scp my-tool user@server:/usr/local/bin/
ssh user@server my-tool sse --port 3000

Docker Deployment

Basic Dockerfile

dockerfile
FROM oven/bun:1

WORKDIR /app

# Install photon CLI
RUN bun add -g @portel/photon

# Copy your photon files
COPY *.photon.ts ./

# Expose MCP SSE port
EXPOSE 3000

# Run as MCP server with SSE transport
CMD ["photon", "sse", "my-photon"]

Multi-Photon Dockerfile

dockerfile
FROM oven/bun:1

WORKDIR /app

# Install photon CLI
RUN bun add -g @portel/photon

# Copy all photons
COPY *.photon.ts ./

# Run Beam UI (serves multiple photons)
EXPOSE 3000
CMD ["photon", "beam", "--port", "3000"]

Docker Compose

yaml
version: '3.8'

services:
  photon:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=info
    volumes:
      - photon-data:/app/.photon
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

volumes:
  photon-data:

Production Recommendations

  1. Use multi-stage builds to minimize image size
  2. Pin dependencies with a lock file
  3. Run as non-root user for security
  4. Mount volumes for persistent data (e.g., SQLite databases)
  5. Set memory limits appropriate for your workload

Cloudflare Workers

Photons deploy to Cloudflare Workers via a Durable Objects bridge. Each photon instance maps 1:1 to a Durable Object (DO), giving it persistent state, hibernation, and edge-local execution without any infrastructure setup.

Deploy

bash
photon host deploy cloudflare my-photon   # alias: photon host deploy cf my-photon

This compiles your photon, generates a wrangler.toml, and deploys via Wrangler in one step.

What the CF Runtime Provides

CapabilityHow it works on CF
this.memoryDO storage backing the photon instance
this.scheduleDO Alarm multiplexer - each scheduled method becomes an alarm
this.call(otherPhoton)Sibling DO binding resolved by photon name
this.assets(path, { load })Synchronously reads files bundled from the photon's companion asset folder
this.cf.* (R2/KV/D1/Queues/Vectorize/AI/Images)Real bindings on the deployed Worker; same shape as local miniflare
this.sample / this.confirm / this.elicitForwarded over the SSE response stream
@get /path / @post /pathDispatched by the Worker fetch handler before MCP routing
@env MY_KEYRead from wrangler.toml [vars] or CF Secrets
--mcp-auth jwtProtect MCP tools/call with signed, scoped JWTs
@auth cf-accessEach CF Access email maps to its own DO instance

For the full this.cf.* reference and the local miniflare sandbox that mirrors a deployed Worker, see CF-BINDINGS.md.

this.assets() resolves the same way locally and on Cloudflare: files in the companion folder named after the photon (my-photon/) are bundled with the Worker. Legacy my-photon/assets/ contents are also copied to the public assets binding for older UI bundles.

Stateful Photons with Durable Objects

Photons with @stateful or this.memory automatically run inside a Durable Object for persistent state. The bridge handles routing:

typescript
export default class TaskBoard {
  /**
   * Add a task to the board
   * @stateful
   */
  async addTask({ title }: { title: string }) {
    const tasks = (await this.memory.get<string[]>('tasks')) ?? [];
    tasks.push(title);
    await this.memory.set('tasks', tasks);
    return { tasks };
  }
}

No wrangler config changes needed - photon host deploy generates the DO binding automatically.

Scheduled Methods on CF

@scheduled methods run as DO Alarms on Cloudflare rather than daemon cron jobs:

typescript
/**
 * Sync external data hourly
 * @scheduled 0 * * * *
 */
async syncData() {
  // Runs as a DO Alarm on CF - no daemon needed
}

HTTP Routes on CF

@get and @post tags work on Cloudflare deployments. The Worker fetch handler dispatches to the annotated method before falling through to MCP routing:

typescript
/**
 * Public iCal feed
 * @get /calendar.ics
 */
async ical(request: Request): Promise<Response> {
  const events = await this.memory.get('events') ?? [];
  return new Response(buildICal(events), {
    headers: { 'Content-Type': 'text/calendar; charset=utf-8' },
  });
}

Workers AI

If your photon uses an @ai constructor parameter, the AI binding is auto-generated in wrangler.toml and injected at runtime:

typescript
export default class Summarizer {
  constructor(
    /** @ai */
    private ai: Ai
  ) {}

  async summarize({ text }: { text: string }) {
    return this.ai.run('@cf/meta/llama-3.1-8b-instruct', {
      prompt: `Summarize: ${text}`,
    });
  }
}

MCP transport-level JWT auth (/mcp)

For new deployments, prefer short-lived scoped JWTs over a shared bearer secret:

bash
photon auth init appointments
photon host deploy cf appointments \
  --mcp-auth jwt \
  --mcp-audience https://appointments.example.com/mcp
photon auth token appointments \
  --agent scheduler \
  --audience https://appointments.example.com/mcp \
  --scope bookings:write \
  --ttl 15m

By default, @readOnly tools require &lt;toolName&gt;:read and other tools require &lt;toolName&gt;:write. Add method-level @scope only when you want a different permission name:

typescript
/**
 * Book a consultation slot.
 * @scope bookings:write
 */
async book(...) {}

When JWT auth is active, tools/call rejects missing or invalid tokens with 401, rejects missing scopes with 403, and populates this.caller from the JWT claims before user code runs. See Securing MCP with JWT for the complete flow.

Legacy MCP bearer auth (/mcp)

By default, /mcp on a deployed photon is open — anyone who knows the URL can hit tools/list and tools/call. For simple or legacy deployments, you can require a shared bearer token by setting the PHOTON_MCP_BEARER secret on the deployed Worker:

bash
wrangler secret put PHOTON_MCP_BEARER
# enter the secret value when prompted

When the secret is set:

  • tools/call (and any non-handshake JSON-RPC method) requires Authorization: Bearer &lt;secret&gt; and returns 401 with WWW-Authenticate: Bearer realm="photon" on missing/wrong bearer.
  • tools/list, initialize, ping, and notifications/* pass through unauthed so MCP clients can complete capability negotiation before authenticating.
  • Bearer comparison is timing-safe.
  • When PHOTON_MCP_BEARER is unset, /mcp stays open (back-compat for existing deployments).

User code can read the auth state via this.mcpAuthed for finer-grained logic:

typescript
async sensitiveMethod() {
  if (!(this as any).mcpAuthed) {
    throw new Error('unauthorized');
  }
  // ...
}

this.mcpAuthed is true only inside a tools/call whose bearer passed; it's false when no secret is configured (i.e., when the gate is off) or outside a /mcp dispatch (e.g., inside a @get/@post handler that uses its own auth).

For per-user identity (multi-tenant routing), use @auth cf-access instead — that maps each authenticated CF Access email to its own DO instance.

Per-User Isolation with CF Access

Add @auth cf-access to route each authenticated Cloudflare Access user to their own DO instance:

typescript
/**
 * Personal task board - one instance per user
 * @auth cf-access
 */
export default class PersonalBoard {
  // Each CF Access email gets its own isolated DO instance
}

Manual wrangler.toml

photon host deploy generates this automatically, but if you need manual control:

toml
name = "my-photon-worker"
main = "dist/worker.js"
compatibility_date = "2024-06-01"

[vars]
ENVIRONMENT = "production"

[[durable_objects.bindings]]
name = "PHOTON_DO"
class_name = "PhotonDurableObject"

[[migrations]]
tag = "v1"
new_classes = ["PhotonDurableObject"]

[[kv_namespaces]]
binding = "PHOTON_KV"
id = "your-kv-id"

Limitations

  • No filesystem access (use this.memory backed by KV or R2)
  • CPU time limit per request (use DO hibernation for long-running work)
  • Bundle size limit of 1 MB compressed

AWS Lambda

Generate Lambda Package

bash
photon host deploy my-photon --target lambda

Manual Setup with SAM

  1. Create template.yaml:
yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  PhotonFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: handler.handler
      Runtime: nodejs20.x
      Timeout: 30
      MemorySize: 256
      Events:
        Api:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: ANY
  1. Build and deploy:
bash
sam build
sam deploy --guided

Lambda Best Practices

  1. Cold start optimization: Keep bundles small, minimize dependencies
  2. Connection reuse: Use keep-alive for database connections
  3. Provisioned concurrency: For consistent latency
  4. Layers: Share dependencies across functions

Systemd Service

For always-on deployment on Linux servers.

Service File

Create /etc/systemd/system/photon.service:

ini
[Unit]
Description=Photon MCP Server
After=network.target

[Service]
Type=simple
User=photon
Group=photon
WorkingDirectory=/opt/photon
Environment=NODE_ENV=production
Environment=LOG_LEVEL=info
ExecStart=/usr/bin/node /usr/local/bin/photon sse my-photon --port 3000
Restart=always
RestartSec=10

# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/opt/photon/.photon

[Install]
WantedBy=multi-user.target

Enable and Start

bash
sudo systemctl daemon-reload
sudo systemctl enable photon
sudo systemctl start photon

View Logs

bash
sudo journalctl -u photon -f

Environment Variables

Photons support configuration via environment variables.

Standard Variables

VariableDescriptionDefault
NODE_ENVEnvironment modedevelopment
LOG_LEVELLog verbosity (error/warn/info/debug)info
PHOTON_DIRData directory~/.photon

Constructor Parameter Injection

Constructor parameters can be injected via environment variables:

typescript
export default class MyPhoton {
  constructor(
    /** @env MY_API_KEY */
    private apiKey: string,
    /** @env MY_TIMEOUT */
    private timeout: number = 30000
  ) {}
}

Set via environment:

bash
export MY_API_KEY=sk-xxx
export MY_TIMEOUT=60000

Health Checks

Photon servers expose health endpoints for monitoring.

SSE Transport

bash
curl http://localhost:3000/health

Beam UI

bash
curl http://localhost:3000/health

Response:

json
{
  "status": "ok",
  "uptime": 3600,
  "photons": 5
}

Monitoring

Structured Logging

Enable JSON logs for log aggregation:

bash
photon sse my-photon --json-logs

Output format:

json
{"level":"info","message":"Tool executed","tool":"search","duration":152,"timestamp":"2024-01-01T00:00:00.000Z"}

Metrics

For production monitoring, consider:

  1. Prometheus: Expose /metrics endpoint
  2. Datadog: Use structured logs with trace IDs
  3. CloudWatch: For AWS deployments

Alerting

Set up alerts for:

  • High error rates (>1% of requests)
  • Slow tool execution (>5s p99)
  • Memory usage (>80% of limit)
  • Connection failures to external services

Next Steps

Released under the MIT License.