---
title: Pulse SDK and Handlers | Vibecodr Docs
description: Use the Pulse SDK, definePulse, defineWebPulse, env.fetch, env.state, and web-standard Request/Response handlers to ship safe backend endpoints.
canonical: https://player.vxbe.space/docs/handlers
---

# Pulse SDK and Handlers

This is the practical reference for writing Pulse backend code: which handler shape to use, what the Pulse SDK gives you, and how env.fetch, env.secrets, env.state, env.request, env.log, and env.runtime fit together.

Marker: Pattern guide for selecting durable Pulse handler conventions.

## Implementation focus

Use this before adding or refactoring backend endpoints so the code is readable, searchable, and explicit about runtime authority.

## Expected outcomes

- Pick the right handler shape for route complexity and ownership.
- Normalize request and response expectations across Pulses.
- Use Pulse runtime capabilities without configuring deployment wiring.
- Reduce long-term maintenance drag from mixed conventions.

## Two ways to write a pulse handler

Pulses support two handler styles. The runtime looks at your default export:

### Enhanced style (recommended)

Function with two parameters: `(input, env)`. Vibecodr parses the JSON request body into `input` and passes a structured `env` object. If you return a plain object, it is serialized as JSON automatically.

Enhanced handler

```typescript
// Default export with (input, env)
export default async function handler(input, env) {
  env.log.info("pulse.received", {
    method: env.request.method,
    hasCity: typeof input?.city === "string",
  });

  return {
    city: typeof input?.city === "string" ? input.city : "unknown",
    units: env.pulse.DEFAULT_UNITS ?? "metric",
  };
}
```

### Web-standard style

Function with one parameter: `(request)`. You work with the standard `Request` object and return a `Response` (or any JSON-serializable value, which will be wrapped for you). Use this for pure HTTP handling. If you need `env.pulse`, secrets, connections, policy fetch, or structured logs, use the enhanced style. Vibecodr wires those capabilities for you; you do not configure deployment wiring yourself.

Web-standard handler

```typescript
// Default export with (request)
export default async function handler(request) {
  const body = await request.json();

  return new Response(
    JSON.stringify({ received: body }),
    {
      status: 200,
      headers: { "Content-Type": "application/json" },
    }
  );
}
```

## How the runtime chooses a style

The runtime picks a style in this order (most explicit wins):

```typescript
// 1) Explicit config override (recommended for wrappers / default params)
export const config = { style: "enhanced" }; // or: "web"

// 2) Wrapper helpers (recommended)
import { definePulse, defineWebPulse } from "@vibecodr/pulse";
export default definePulse(async (input, env) => ({
  processed: true,
  source: env.pulse.SOURCE ?? "pulse",
}));
// export default defineWebPulse(async (req) => Response.json({ receivedMethod: req.method }));

// 3) Fallback heuristic (avoid relying on this)
// enhanced: function.length === 2
export default async function(input, env) { ... }
// web: function.length === 1
export default async function(request) { ... }
```

Prefer `definePulse()` / `defineWebPulse()` so refactors (wrappers, default params) never accidentally flip your handler into the wrong mode.

## PulseEnv (env) quick reference

In enhanced style, the second argument is PulseEnv. `input` is what the caller sent after parsing, and `env.request` gives you sanitized request access when you need headers, method, or URL details.

#### Inspect capabilities safely

When a Pulse needs to confirm what Vibecodr exposed to it, use `env.runtime.capabilities()`. It returns a safe, creator-facing snapshot of things like declared secret names, connected provider ids, public `.pulse` keys, configured raw-body routes, and named Pulse State resources without exposing private runtime internals or secret values.

PulseEnv surface

```typescript
import { definePulse, type PulseEnv } from "@vibecodr/pulse";

type WeatherInput = { city: string };
type WeatherContext = { source?: string };
type WeatherConfig = { API_BASE_URL?: string };

type Env = PulseEnv<WeatherInput, WeatherContext, WeatherConfig>;

export default definePulse(async (input: WeatherInput, env: Env) => {
  const runtimeCaps = env.runtime.capabilities();

  env.log.info("weather.requested", { city: input.city });

  const response = await env.fetch(
    new URL("/weather", env.pulse.API_BASE_URL ?? "https://api.example.com")
  );

  return Response.json({
    ok: response.ok,
    pulseId: env.runtime.pulseId,
    requestId: env.runtime.requestId,
    routeId: env.runtime.routeId ?? null,
    declaredSecrets: runtimeCaps.secrets.declaredKeys,
  });
});
```

input

What the caller sent after the runtime parses the request payload.

env.secrets

Private setup capabilities for provider calls. Start with `env.fetch(..., { auth: env.secrets.bearer("KEY") })`.

env.connections

Connected accounts injected server-side for OAuth-backed APIs.

env.fetch

Policy outbound fetch with Vibecodr-managed egress controls.

env.pulse

Non-secret runtime constants from the capsule's `.pulse` file.

env.log

Structured runtime logs for request/run observability.

env.waitUntil()

Advanced best-effort after-response work, not durable background processing.

env.event

Event envelope metadata (`{ type, payload }`) for manual runs and automations.

env.runtime

Safe correlation metadata (`pulseId`, `deploymentId`, `requestId`, `traceId`) plus `env.runtime.capabilities()` for safe capability inspection, never authorization.

env.request

Sanitized metadata for method, headers, and URL. Raw body reads are explicit under `env.request.raw`.

env.state

Named Pulse State resources for operation coordination across Pulse calls: `runOnce()`, `claim()`, `keyFromRequest()`, and guarded effects.

Descriptor-declared state resource

```json
{
  "apiVersion": "pulse/v1",
  "setup": {
    "state": {
      "stripeWebhookEvents": {
        "kind": "idempotency",
        "retentionTtlSeconds": 86400,
        "replay": {
          "mode": "json",
          "maxBytes": 4096
        }
      }
    }
  }
}
```

Protected idempotent work

```typescript
export default async function handler(input, env) {
  return env.state.stripeWebhookEvents.runOnce(input.eventId, async () => {
    await chargeCustomer();

    // Only awaited work inside runOnce() is protected completion.
    return { accepted: true };
  });
}
```

Use `env.state` when the Pulse needs to coordinate backend behavior across multiple invocations. Pulse State tracks operation lifecycle, duplicate admission, pending/completed status, and guarded effects. For durable app records, use a connected service or an explicit data surface; for safe coordination around a protected operation, use Pulse State. `env.waitUntil()`, timers, and fire-and-forget work run outside protected completion. Completed duplicates return stored small JSON-safe callback results when replay was retained; unsafe, oversized, or non-JSON results fail with `PulseStateReplayUnavailable` instead of running creator code again.

#### Scope of a state key

Pulse State coordinates a key inside a named resource for one Pulse scope. Multiple calls with the same resource and key coordinate with each other; calls with different keys are separate operations. That makes the key the product decision: it should name the operation you want to protect.

- Good key: `stripe:event:evt_123` for a retried webhook event.

- Good key: `user:42:form:contact:abc` for a client retry with an idempotency token.

- Weak key: `Date.now()`, because every call becomes a different operation.

#### How long Pulse State remembers a key

A state resource may declare `retentionTtlSeconds`. That is how long Vibecodr keeps duplicate-memory for completed or terminal operations on that resource, capped by the owner's plan.

Free

Up to 24 hours of Pulse State duplicate-memory.

Creator

Up to 7 days of Pulse State duplicate-memory.

Pro

Up to 30 days of Pulse State duplicate-memory.

Retention keeps a minimized coordination record: protected operation identity, status, timing, and small replay/failure metadata. Raw keys, secrets, request bodies, and application records do not become creator-readable state. After the retention window, the record no longer counts as duplicate memory and becomes eligible for Vibecodr cleanup, so do not use Pulse State as long-term app storage.

Vibecodr persists that coordination record in durable platform storage. It is not held by keeping a live worker warm, so a cold start does not erase duplicate memory. The retained record mostly costs tiny stored metadata; reads, writes, and runtime work are paid when calls or cleanup touch the record. Cleanup runs in bounded batches; exact byte-removal timing can lag the semantic retention window, so plan caps and cleanup retries keep the storage footprint bounded.

#### What counts as a Pulse State operation?

A Pulse State operation is one admitted attempt to guard a key through a descriptor-declared `env.state` idempotency resource. It is counted before protected work runs, so Vibecodr can coordinate duplicate webhooks or retried HTTP mutations before creator code performs the protected operation.

- Use one for awaited work inside `runOnce()` or an acquired manual lifecycle.

- Think of it as an operation guard. App records, search/query data, and files belong in a data surface or connected service.

- The monthly quota is separate from Pulse runs and protects the shared coordination lane from unbounded retry cost.

- Creator code cannot list, delete, purge, or reset Pulse State records. Account and project cleanup are handled through separate audited product flows.

#### Choose among the Pulse State tools

`runOnce()` fits the common case, but Pulse State is a small toolkit for coordinating a protected operation. Pick the tool that matches how much lifecycle control the Pulse needs.

`runOnce(key, handler)`

Use this when one awaited callback contains the work. Vibecodr guards the key, runs the callback only for the acquired attempt, then completes or fails the operation.

Manual lifecycle

Use `claim(key)` when the Pulse needs to decide when to call `complete()` or `fail()`. The handle is usable only inside the current invocation.

`keyFromRequest(request)`

Use this for HTTP mutations that carry one trusted idempotency key. Verify webhook signatures before deriving keys from provider events.

`effect()` and `effectKey()`

Use these inside an acquired `runOnce()` or manual lifecycle to pass provider-ready idempotency metadata to guarded Stripe or HTTP side effects.

#### Using Pulse State operations safely

Why use it

Protect work where retries or duplicate deliveries could charge, send, or mutate something twice.

When to use it

Use it for webhooks, POST mutations, and retryable requests with one stable idempotency key.

How to use it safely

Declare the resource, derive the key from trusted input, and keep protected work awaited inside `runOnce()`.

- Verify webhooks before deriving the key.

- Use `keyFromRequest()` for HTTP mutations that require an `Idempotency-Key` header.

- Only awaited work inside `runOnce()` is protected; `waitUntil()`, timers, and fire-and-forget promises run outside the protected operation.

- Duplicate pending requests should not run creator code again.

Verified webhook Pulse State operation

```typescript
import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (_input, env) => {
  const event = await env.webhooks.verify("stripe", {
    secret: "STRIPE_WEBHOOK_SECRET",
  });

  return env.state.stripeWebhookEvents.runOnce(event.id, async () => {
    await env.fetch("https://api.example.com/fulfill", {
      method: "POST",
      auth: env.secrets.bearer("PROVIDER_API_KEY"),
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ eventId: event.id }),
    });

    return { accepted: true };
  });
});
```

.pulse file

```ini
# .pulse
# Non-secret values only. Put API keys and tokens in Pulse Secrets.
PUBLIC_GREETING=Hello from your pulse
FEATURE_FLAG=true
```

Read non-secret values

```typescript
import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (_input, env) => {
  return {
    greeting: env.pulse.PUBLIC_GREETING ?? "Hello",
    featureEnabled: env.pulse.FEATURE_FLAG === "true",
  };
});
```

Studio creates `.pulse` when you add pulse endpoint files. It is private source for the owner and deploy pipeline, not a public metadata surface, and secret-looking names or values are rejected at deploy time.

PulseSecrets interface

```typescript
type PulseSecrets = {
  has(key: string): boolean;
  keys(): string[];
  bearer(key: string): PulseAuth;
  header(key: string, headerName: string, format?: string): PulseHeaderAuth;
  query(key: string, paramName: string): PulseQueryAuth;
  verifyHmac(key: string, input: PulseHmacVerificationInput): Promise<PulseHmacVerificationResult>;

  // Advanced compatibility only. Beginner Pulses should use env.fetch(..., { auth }).
  get(key: string): string;
  fetch(key: string, options: SecretFetchOptions): Promise<Response>;
};
```

PulseConnections interface

```typescript
type PulseConnectionClient = {
  fetch(input: string | URL | Request, init?: PulseConnectionFetchInit): Promise<Response>;
};

type PulseConnections = {
  use(providerId: string): PulseConnectionClient;
  auth(providerId: string): PulseConnectionAuth;
};
```

Example: policy fetch + structured logs

```typescript
import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (input, env) => {
  const city = typeof input?.city === "string" ? input.city.trim() : "";
  if (!city) {
    return { error: "city_required" };
  }

  const units = env.pulse.DEFAULT_UNITS ?? "metric";
  env.log.info("weather.lookup.started", { city, units });

  const response = await env.fetch(
    `${env.pulse.WEATHER_API_BASE_URL ?? "https://weather.example.com"}/forecast?city=${encodeURIComponent(city)}&units=${units}`
  );

  return {
    city,
    units,
    upstreamStatus: response.status,
    callerMethod: env.request.method,
  };
});
```

Advanced: env.db compatibility

```typescript
// env.db is an advanced compatibility surface on eligible plans.
// It is not the default beginner model and does not imply automatic Pulse State migration.
if (env.db) {
  const { results } = await env.db.query(
    "SELECT id, status FROM support_tickets WHERE owner_id = ? LIMIT 20",
    [ownerId]
  );

  return { tickets: results };
}
```

Secrets + policy fetch: Start with `env.fetch(..., { auth: env.secrets.bearer("KEY") })` when an upstream call needs a secret. Keep outbound calls on `env.fetch()` so policy controls stay in place.

## Request body -> input mapping (enhanced style)

Enhanced handlers always receive `input` (the payload) and a PulseEnv object. The event envelope is available on `env.event`.

API run endpoint (most common)

```bash
# Published pulse: direct call by default
curl -sS https://api.vibecodr.space/pulses/pls_abc123/run \
  -H "Content-Type: application/json" \
  -d '{"city":"San Francisco"}'
```

Your handler receives `input` as `{ city: "San Francisco" }`, and `env.event.type` is `manual_run`.

## Structured route body

### Pulse SDK entrypoints

The Pulse SDK gives backend code two explicit entrypoints. Use definePulse() when the handler needs Vibecodr runtime capabilities such as env.fetch, env.secrets, env.connections, env.state, env.request, env.log, or env.runtime. Use defineWebPulse() when the endpoint is plain HTTP and should work like a standard Request/Response handler.

env.runtime stays the safe runtime metadata lane. When a Pulse needs to inspect what Vibecodr exposed to it, use env.runtime.capabilities() instead of probing raw env fields or private runtime internals.

The important rule is explicitness: a production Pulse should not depend on function argument guessing to decide whether it receives input and env or a standard Request.

- definePulse((input, env) => ...) receives parsed JSON input plus the curated Pulse runtime API.
- defineWebPulse((request) => ...) receives the web-standard Request and returns a Response.
- env.fetch keeps outbound network calls inside Vibecodr policy controls.
- env.state coordinates operation lifecycle across Pulse calls with named resources and stable keys.

#### Enhanced Pulse SDK handler

```typescript
import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (input, env) => {
  const city = typeof input?.city === "string" ? input.city : "Paris";
  const runtimeCaps = env.runtime.capabilities();

  const forecast = await env.fetch(
    `https://weather.example.com/forecast?city=${encodeURIComponent(city)}`
  );

  env.log.info("forecast.loaded", {
    city,
    status: forecast.status,
    declaredSecrets: runtimeCaps.secrets.declaredKeys,
  });

  return {
    city,
    ok: forecast.ok,
    requestId: env.runtime.requestId,
  };
});
```

#### Web-standard handler

```typescript
import { defineWebPulse } from "@vibecodr/pulse";

export default defineWebPulse(async (request) => {
  if (request.method !== "POST") {
    return Response.json(
      { error: "method_not_allowed" },
      { status: 405 }
    );
  }

  const body = await request.json().catch(() => null);
  return Response.json({ received: body });
});
```

### What a Pulse State operation counts

A Pulse State operation is one admitted attempt to use a descriptor-declared env.state resource. It is counted when the Pulse asks Vibecodr to guard a key before protected work runs, so duplicate webhooks or retried HTTP mutations can be coordinated before creator code performs the protected operation.

Pulse State operations are separate from Pulse runs. A run can use zero Pulse State operations, one operation, or more than one when it intentionally protects multiple idempotency resources.

runOnce() is scoped to the resource plus key inside the Pulse owner/deployment scope. Multiple calls with the same resource and key meet at the same coordination record.

- Use a Pulse State operation for awaited work inside runOnce() or an acquired manual lifecycle.
- Think of Pulse State as an operation guard. App records, search/query data, and files belong in a data surface or connected service.
- The monthly quota protects the shared coordination lane from unbounded retry and duplicate-key cost.
- If duplicate protection is unnecessary, skip env.state and the run will not spend a Pulse State operation.
- Creator code cannot list, delete, purge, or reset Pulse State records. Account and project cleanup are handled through separate audited product flows.

### How long Pulse State remembers a key

A state resource may declare retentionTtlSeconds. That is how long Vibecodr keeps duplicate-memory for completed or terminal operations on that resource, capped by the owner's plan.

Free plans can retain Pulse State duplicate-memory for up to 24 hours, Creator plans for up to 7 days, and Pro plans for up to 30 days. If a resource asks for a longer window, Vibecodr caps it to the plan limit.

Retention keeps a minimized coordination record: protected operation identity, status, timing, and small replay/failure metadata. Raw keys, secrets, request bodies, and application records do not become creator-readable state.

Vibecodr persists that coordination record in durable platform storage. It is not held by keeping a live worker warm, so a cold start does not erase duplicate memory.

- retentionTtlSeconds controls duplicate-memory retention for the state resource, not the lifetime of user data.
- The retained record mostly costs tiny stored metadata; reads, writes, and runtime work are paid when calls or cleanup touch the record.
- After the retention window, the record no longer counts as duplicate memory and becomes eligible for Vibecodr cleanup; do not use Pulse State as long-term app storage.
- Cleanup runs in bounded batches. Exact byte-removal timing can lag the semantic retention window, so plan caps and cleanup retries keep the storage footprint bounded.
- Owner/account and Pulse deletion use separate audited cleanup paths and are not exposed through the creator runtime SDK.

### Choose among the Pulse State tools

runOnce() fits the common case, but Pulse State is a small toolkit for coordinating a protected operation. Pick the tool that matches how much lifecycle control the Pulse needs.

A manual lifecycle is useful when the Pulse needs to decide when to complete or fail a guarded operation instead of wrapping all protected work in one callback.

- runOnce(key, handler): use this when one awaited callback contains the work. Vibecodr guards the key, runs the callback only for the acquired attempt, then completes or fails the operation.
- Manual lifecycle: use claim(key) when the Pulse needs to call complete() or fail() itself. The handle is usable only inside the current invocation.
- keyFromRequest(request): use this for HTTP mutations that carry one trusted idempotency key. Verify webhook signatures before deriving keys from provider events.
- effect() and effectKey(): use these inside an acquired runOnce() or manual lifecycle to pass provider-ready idempotency metadata to guarded Stripe or HTTP side effects.

#### Manual lifecycle

```typescript
const claim = await env.state.orderMutations.claim(orderId);

if (claim.status === "completed") {
  return claim.replay;
}
if (claim.status === "pending") {
  return { accepted: true, status: "already-running" };
}

let protectedWorkSucceeded = false;
try {
  const effect = await claim.effect({
    provider: "stripe",
    operation: "payment_intents.capture",
    target: paymentIntentId,
  });

  await env.fetch(`https://api.stripe.com/v1/payment_intents/${paymentIntentId}/capture`, {
    method: "POST",
    headers: effect.headers,
    auth: env.secrets.bearer("PAYMENT_PROVIDER_API_KEY"),
  });
  protectedWorkSucceeded = true;
} catch (error) {
  if (!protectedWorkSucceeded) {
    await claim.fail(error);
  }
  throw error;
}

await claim.complete({ accepted: true });
return { accepted: true };
```

### Using Pulse State operations safely

Why use it: Pulse State operations exist for work where a retry or duplicate delivery could charge a customer twice, send the same email twice, or mutate a provider twice. Pulse State lets Vibecodr coordinate the key before creator code performs the protected operation.

When to use it: use env.state for webhook events, POST mutations, and other retryable requests that carry one stable idempotency key. Skip it for ordinary reads, analytics-only logs, or work where running twice is harmless.

How to use it safely: declare the state resource, derive the key from a trusted source, and keep the protected work awaited inside runOnce(). Verify webhooks before deriving the key, because an unverified body is not trusted input. Duplicate protection makes repeated wakes safe; it is not an exactly-once guarantee for third-party side effects.

- Verify webhooks before deriving the key.
- Use keyFromRequest() for generic HTTP mutations that require an Idempotency-Key header.
- Only awaited work inside runOnce() is protected; waitUntil(), timers, and fire-and-forget promises run outside the protected operation.
- Duplicate pending requests should not run creator code again.
- Completed duplicates return the stored small JSON-safe callback result when replay was retained. If replay was disabled or omitted because the result was unsafe, oversized, or not JSON-safe, runOnce() fails with PulseStateReplayUnavailable instead of running creator code again.

#### Verified webhook Pulse State operation

```typescript
import { definePulse } from "@vibecodr/pulse";

export default definePulse(async (_input, env) => {
  const event = await env.webhooks.verify("stripe", {
    secret: "STRIPE_WEBHOOK_SECRET",
  });

  return env.state.stripeWebhookEvents.runOnce(event.id, async () => {
    await env.fetch("https://api.example.com/fulfill", {
      method: "POST",
      auth: env.secrets.bearer("PROVIDER_API_KEY"),
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ eventId: event.id }),
    });

    return { accepted: true };
  });
});
```

### Production handler conventions

A handler convention is a team contract. Once client code depends on response fields, status codes, or route names, changing them becomes a public API change.

Prefer small handlers that compose clear helpers over large route files. Method checks, validation, idempotency, external calls, and response shaping should be quick to find without scrolling through unrelated behavior.

- Check request method before reading expensive bodies.
- Parse and validate input before calling external services.
- Keep route names stable.
- Version payloads when callers need compatibility.
- Log metadata, not raw secrets, tokens, or private user payloads.

### Example and read next

Example: a Pulse receives POST /api/contact. Use a web-standard Request handler, parse JSON once, validate required fields, call trusted providers through env capabilities, and return a viewer-safe Response.

Use these related pages when you need the next layer of guidance. They point to the most likely follow-up tasks, not every page that happens to touch the same system.

- Read next: [/blueprints](/blueprints)
- Read next: [Vibes & Pulses](/docs/vibes-pulses)
- Read next: [Pulse Routing](/docs/pulse-routing)
- Read next: [Secrets & APIs](/docs/secrets)

## Related documentation

- [/blueprints](https://player.vxbe.space/blueprints)
- [/docs/vibes-pulses](https://player.vxbe.space/docs/vibes-pulses)
- [/docs/pulse-routing](https://player.vxbe.space/docs/pulse-routing)
- [/docs/secrets](https://player.vxbe.space/docs/secrets)