Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ website/.vitepress/cache/
website/.vitepress/dist/

# test coverage
coverage
coverage
.vercel
.env*.local
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ pnpm --filter @coji/durably test:node:all # Run all Node tests (SQLite +
pnpm db:down # Stop Postgres
```

## Codex CLI for Research & Consultation

When stuck or needing up-to-date information about external services, use Codex CLI (`codex exec`) to consult GPT. Codex can perform web searches, making it especially useful for real-time information like Vercel configuration, latest library APIs, or deployment troubleshooting.

```bash
codex exec "your question or prompt here"
```

- Run in the background and check results later for non-blocking workflow
- Include relevant codebase context in the prompt for better answers
- Useful for research, design consultation, and debugging — not just search
- **During `/review` and `/simplify`**: Run `codex exec` in parallel with the review agents to get a second opinion on the diff. Pass the diff content and ask for code quality feedback, potential issues, or improvement suggestions

## Skills

- **release-check** - Pre-release integrity check for API changes and spec updates (`.claude/skills/release-check/`)
Expand Down
7 changes: 7 additions & 0 deletions examples/fullstack-vercel-turso/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Local development (libsqld via Docker)
TURSO_DATABASE_URL=http://localhost:8080

# Production (Turso)
# TURSO_DATABASE_URL=libsql://your-db-name.turso.io
# TURSO_AUTH_TOKEN=your-token
# CRON_SECRET=your-secret
11 changes: 11 additions & 0 deletions examples/fullstack-vercel-turso/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.DS_Store
.env
/node_modules/

# React Router
/.react-router/
/build/

*.swp
.vercel
.env*.local
13 changes: 13 additions & 0 deletions examples/fullstack-vercel-turso/.vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Exclude sibling examples to prevent @vercel/react-router builder
# from scanning other React Router apps in the monorepo
../../examples/fullstack-react-router
../../examples/spa-vite-react
../../examples/spa-react-router
../../examples/server-node

# Exclude non-essential workspace packages
../../website

# Exclude tests and docs
../../packages/durably/tests
../../packages/durably-react/tests
78 changes: 78 additions & 0 deletions examples/fullstack-vercel-turso/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Fullstack Vercel + Turso Example

Durably fullstack demo deployed on Vercel with Turso (libSQL) as the database.

> **Note**: This is a demo app with no authentication on the Durably control plane.
> For production use, add authentication via `createDurablyHandler({ authenticate })`.
> See the [auth guide](../../website/guide/auth.md) for details.

## Architecture

- **Framework**: React Router v7 with `@vercel/react-router` preset
- **Database**: Turso (remote libSQL) in production, local libsqld via Docker in development
- **Worker**: Dual-mode
- **Real-time**: `onRequest` lazily starts the worker — runs during SSE streaming
- **Background**: `/api/worker` endpoint called by Vercel Cron (requires Pro plan for per-minute schedule)

## How it works

```text
User triggers job → POST /api/durably/trigger
User subscribes → GET /api/durably/subscribe?runId=xxx (SSE)
onRequest → durably.init() starts worker during SSE connection
Worker processes → steps stream via SSE in real-time
SSE disconnects → function terminates, worker stops
Vercel Cron → GET /api/worker processes any remaining pending jobs
```

## Setup

### Local development

Requires Docker for the local libsqld instance.

```bash
cp .env.example .env
pnpm install
pnpm dev
```

This starts a local libsqld container (Docker) and the dev server.
The app connects to `http://localhost:8080` — same HTTP protocol as production Turso.

### Production (Vercel + Turso)

1. Create a Turso database:

```bash
turso db create my-durably-app
turso db tokens create my-durably-app
```

2. Set environment variables in Vercel:
- `TURSO_DATABASE_URL` — `libsql://my-durably-app-<user>.turso.io`
- `TURSO_AUTH_TOKEN` — token from step 1
- `CRON_SECRET` — any random string to authenticate cron requests

3. Deploy:
```bash
vercel
```

## Key files

| File | Description |
| ----------------------------- | ------------------------------------------- |
| `docker-compose.yml` | Local libsqld for development |
| `app/lib/database.server.ts` | Turso/libSQL connection config |
| `app/lib/durably.server.ts` | Durably instance with `onRequest` lazy init |
| `app/lib/durably.ts` | Type-safe client for React components |
| `app/routes/api.durably.$.ts` | Durably HTTP/SSE handler (splat route) |
| `app/routes/api.worker.ts` | Background worker endpoint for Vercel Cron |
| `vercel.json` | Cron schedule (per-minute requires Pro) |
| `react-router.config.ts` | Vercel preset configuration |
12 changes: 12 additions & 0 deletions examples/fullstack-vercel-turso/app/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@import 'tailwindcss';

@theme {
--font-sans:
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}

html,
body {
@apply bg-gray-50;
}
55 changes: 55 additions & 0 deletions examples/fullstack-vercel-turso/app/jobs/data-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* Data Sync Job
*
* Simulates syncing data with a remote server.
*/

import { defineJob } from '@coji/durably'
import { z } from 'zod'
import { delay } from './delay'

export const dataSyncJob = defineJob({
name: 'data-sync',
input: z.object({ userId: z.string() }),
output: z.object({ synced: z.number(), failed: z.number() }),
run: async (step, input) => {
step.log.info(`Starting sync for user: ${input.userId}`)

const items = await step.run('fetch-local', async () => {
step.progress(1, 4, 'Fetching local data...')
await delay(300)
return Array.from({ length: 10 }, (_, i) => ({
id: `item-${i}`,
data: `Data for ${input.userId}`,
}))
})

let synced = 0
let failed = 0

for (let i = 0; i < items.length; i++) {
const item = items[i]
const success = await step.run(`sync-item-${item.id}`, async () => {
step.progress(2 + Math.floor(i / 5), 4, `Syncing item ${i + 1}...`)
await delay(100)
return Math.random() > 0.1 // 90% success rate
})

if (success) {
synced++
} else {
failed++
step.log.warn(`Failed to sync item: ${item.id}`)
}
}

await step.run('finalize', async () => {
step.progress(4, 4, 'Finalizing...')
await delay(200)
})

step.log.info(`Sync complete: ${synced} synced, ${failed} failed`)

return { synced, failed }
},
})
Comment on lines +11 to +55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

# 1) Show where the dashboard fetches and renders step history.
rg -n -C2 'getSteps\(|Steps:' examples/fullstack-vercel-turso/app/routes/_index/dashboard.tsx

echo
# 2) Show preserveSteps / cleanupSteps semantics in the runtime or docs.
rg -n -C3 'preserveSteps|cleanupSteps' packages/durably/src docs || true

echo
# 3) Show whether the example jobs opt into step retention.
rg -n -C2 'preserveSteps' examples/fullstack-vercel-turso/app/jobs || true

Repository: coji/durably

Length of output: 4881


Add preserveSteps: true to preserve step history for the dashboard.

The dashboard fetches and displays step history for completed runs, but this job uses the default cleanup behavior that deletes steps when runs reach a terminal state. Add preserveSteps: true to the job definition to preserve step records, and apply the same setting to the other example jobs the dashboard displays.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/fullstack-vercel-turso/app/jobs/data-sync.ts` around lines 12 - 56,
The job definition for dataSyncJob uses defineJob but doesn’t set preserveSteps,
so step history will be cleaned up; update the defineJob call for dataSyncJob to
include preserveSteps: true in its configuration object (and apply the same flag
to other example jobs) so the dashboard can retain step records for completed
runs — locate the dataSyncJob definition and add the preserveSteps: true
property alongside name/input/output/run.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional — we don't want to preserve steps for this demo. Step data is cleaned up on terminal state by design (default behavior). The dashboard's View button fetches steps, which will show empty for completed runs, and that's fine for demonstrating the default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coji, understood! Demonstrating the default cleanup behavior is a perfectly valid choice for this example — showing that step history is empty for completed runs is itself informative for users exploring the defaults.


✏️ Learnings added
Learnt from: coji
Repo: coji/durably PR: 105
File: examples/fullstack-vercel-turso/app/jobs/data-sync.ts:11-55
Timestamp: 2026-03-12T13:18:37.201Z
Learning: In `examples/fullstack-vercel-turso`, all example jobs (dataSyncJob, generateReportJob, importCsvJob, processImageJob) intentionally omit `preserveSteps: true` to demonstrate the default behavior where step history is deleted when runs reach a terminal state. The dashboard's View button showing empty steps for completed runs is acceptable and by design. Do not suggest adding `preserveSteps: true` to these jobs.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: coji
Repo: coji/durably PR: 101
File: docs/rfcs/runtime-rearchitecture/ja/core-runtime.md:0-0
Timestamp: 2026-03-08T05:06:39.050Z
Learning: Reviewers should verify that step execution uses step.run() and that step output data is persisted. By default, preserveSteps is false (step history is deleted when runs reach a terminal state). If audit/debug history is required, ensure preserveSteps is set to true. Be aware that cleanupSteps used to exist with inverted semantics (cleanupSteps: true meant delete), and this was renamed to preserveSteps in Phase 1. Apply this guidance to all JS/TS code and docs that reference step persistence.

Learnt from: coji
Repo: coji/durably PR: 11
File: examples/browser-vite-react/src/components/dashboard.tsx:24-34
Timestamp: 2026-01-02T14:03:37.741Z
Learning: In example code directories (e.g., examples/browser-vite-react, examples/fullstack-react-router), error handling may be intentionally omitted to keep code simple and readable, focusing on demonstrating the Durably API rather than production best practices.

1 change: 1 addition & 0 deletions examples/fullstack-vercel-turso/app/jobs/delay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const delay = (ms: number) => new Promise((r) => setTimeout(r, ms))
Loading
Loading