- name
- resonate-lovable-usage-prompt-typescript
- description
- Specialized guidance for building Resonate applications within Lovable.dev, which is Node.js/React/TypeScript. Use when working with Lovable's AI-assisted development environment to scaffold, iterate, and deploy Resonate workflows in the TypeScript SDK.
- license
- Apache-2.0
# Resonate Lovable Usage Prompt — TypeScript
> **SDK version:** This skill reflects `@resonatehq/sdk` v0.11.4 (current on npm).
## Overview
This skill provides specialized guidance for building Resonate durable execution applications within [Lovable.dev](https://lovable.dev), an AI-assisted full-stack development environment. Lovable enables rapid prototyping and deployment of React + Node.js applications with built-in hosting.
## Core Philosophy: Resonate IS the Database
**Critical insight:** For workflow state, Resonate is your single source of truth. You don't need a separate database to track workflow status, pending approvals, or execution state.
```
Traditional approach (unnecessary complexity):
Create workflow → Store in DB → Poll DB for status → Sync with Resonate
Resonate approach (simple):
Create workflow → Query Resonate directly → Done
```
**Why this matters:**
- Query promises by ID prefix to list pending items
- Resolve promises to complete workflows
- No DB sync logic, no stale state, no race conditions
**When you DO need a database:**
- Business data that outlives workflows (user profiles, orders, products)
- Analytics and reporting
- Data that needs relational queries
**When you DON'T need a database:**
- Workflow status (use Resonate promises)
- Pending approvals (query by prefix)
- Execution state (Resonate tracks this)
## Lovable Environment Constraints
### What Lovable Provides
- React frontend (Vite + TypeScript)
- Node.js backend (Express + TypeScript)
- PostgreSQL database (Supabase)
- Built-in deployment pipeline
- AI-assisted code generation
- Git integration
### Lovable Limitations for Resonate
**Lovable does NOT provide:**
- Long-running backend processes
- WebSocket servers
- Background workers
- Cron jobs or schedulers
**Implication:** You cannot run a persistent Resonate worker inside Lovable's backend.
## Recommended Architecture for Lovable + Resonate
### Architecture A: External Resonate Server (Recommended)
**Lovable backend acts as HTTP client to external Resonate:**
```
Lovable Frontend (React)
↓
Lovable Backend (Express) [HTTP Client Only]
↓
External Resonate Server (Cloud Run, Fly.io, Railway)
↓
Resonate Workers (separate deployment)
```
**Lovable backend code:**
```typescript
// server/routes/workflows.ts
import { Resonate } from "@resonatehq/sdk";
import express from "express";
const router = express.Router();
// Connect to external Resonate server
const resonate = new Resonate({
url: process.env.RESONATE_URL, // e.g., https://resonate.example.com
token: process.env.RESONATE_TOKEN, // JWT token for auth (if server requires it)
group: "lovable-client"
});
// Start a workflow (non-blocking)
router.post("/api/workflows/start", async (req, res) => {
const { workflowType, input } = req.body;
const workflowId = `workflow-${Date.now()}`;
await resonate.beginRpc(
workflowId,
workflowType,
input,
resonate.options({
target: "poll://any@workers" // Routes to external workers
})
);
res.json({
workflowId,
status: "started",
pollUrl: `/api/workflows/${workflowId}/status`
});
});
// Check workflow status
router.get("/api/workflows/:id/status", async (req, res) => {
try {
const handle = await resonate.get(req.params.id);
const result = await handle.result();
res.json({
workflowId: req.params.id,
status: "completed",
result
});
} catch (error) {
res.json({
workflowId: req.params.id,
status: "pending"
});
}
});
export default router;
```
**External workers (deployed separately):**
```typescript
// workers/index.ts (deployed to Cloud Run/Fly.io)
import { Resonate, type Context } from "@resonatehq/sdk";
const resonate = new Resonate({
url: process.env.RESONATE_URL,
token: process.env.RESONATE_TOKEN, // JWT token for auth
group: "workers"
});
function* processOrder(ctx: Context, input: any) {
// Durable workflow logic
const validated = yield* ctx.run(validateOrder, input);
const payment = yield* ctx.run(processPayment, validated);
const shipment = yield* ctx.run(createShipment, payment);
return { status: "success", shipment };
}
resonate.register("processOrder", processOrder);
// Keep worker alive
process.on("SIGTERM", () => process.exit(0));
```
### Architecture B: Supabase Edge Functions (Alternative)
If using Lovable with Supabase Edge Functions, see the **resonate-supabase-deployments-typescript** skill for complete Deno-specific patterns including:
- `@resonatehq/supabase` shim usage
- `start/` and `probe/` endpoint patterns
- Deno.serve() request handling
**Limitation:** Supabase Edge Functions have 30-second timeout, so workflows must complete quickly or use async patterns.
## HTTP API vs SDK: When to Use Which
This is the most important decision when building with Lovable + Resonate.
### Use the SDK (Programming Model) When:
| Action | SDK Method | Example |
|--------|------------|---------|
| Start a workflow | `resonate.run()`, `resonate.rpc()`, `beginRun()`, `beginRpc()` | Starting an order processing workflow |
| Execute durable code | Generator functions with `ctx.run()`, `ctx.sleep()` | The workflow logic itself |
| Register workflow handlers | `resonate.register()` | Setting up workers |
**Key insight:** The SDK is for **executing workflows**. You need a process that can run the workflow code.
### Use the HTTP API Directly When:
| Action | HTTP Method | Example |
|--------|-------------|---------|
| List promises by prefix | `GET /promises?id=prefix-*` | Showing all pending approvals in UI |
| Get promise state | `GET /promises/{id}` | Checking if a workflow completed |
| Resolve a HITL promise | `PATCH /promises/{id}` | User clicking "Approve" button |
| Create a standalone promise | `POST /promises` | External system creating a promise to be resolved later |
**Key insight:** The HTTP API is for **managing promise state** without running workflow code.
### Decision Flowchart
```
Do you need to RUN workflow code (generators, ctx.run, ctx.sleep)?
│
├─ YES → Use SDK: resonate.run(), resonate.rpc(), etc.
│ (Requires a worker process that can execute the code)
│
└─ NO → Are you querying or resolving existing promises?
│
├─ YES → Use HTTP API: GET/PATCH /promises
│ (Can be done from any HTTP client)
│
└─ NO → You probably need the SDK
```
### Lovable-Specific Guidance
Since Lovable **cannot run persistent workers**, your Lovable backend should:
1. **Use SDK** to START workflows on external workers: `resonate.beginRpc()` with `target: "poll://any@workers"`
2. **Use HTTP API** to QUERY workflow state: `GET /promises?id=...`
3. **Use HTTP API or SDK** to RESOLVE promises: `PATCH /promises/{id}` or `resonate.promises.resolve(id, { data })`
The actual workflow EXECUTION happens on your external workers (Cloud Run, Fly.io, etc.), not in Lovable.
## Lovable-Specific Patterns
### Pattern 1: Async Workflow with Polling
**Frontend (React):**
```tsx
// src/components/WorkflowRunner.tsx
import { useState } from "react";
import { useToast } from "@/components/ui/use-toast";
export function WorkflowRunner() {
const [workflowId, setWorkflowId] = useState<string | null>(null);
const [status, setStatus] = useState<"idle" | "running" | "completed">("idle");
const { toast } = useToast();
const startWorkflow = async () => {
setStatus("running");
const response = await fetch("/api/workflows/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
workflowType: "processOrder",
input: { orderId: "123", items: [...] }
})
});
const { workflowId } = await response.json();
setWorkflowId(workflowId);
// Poll for completion
const pollInterval = setInterval(async () => {
const statusRes = await fetch(`/api/workflows/${workflowId}/status`);
const data = await statusRes.json();
if (data.status === "completed") {
clearInterval(pollInterval);
setStatus("completed");
toast({ title: "Workflow completed!", description: JSON.stringify(data.result) });
}
}, 2000);
};
return (
<div>
<button onClick={startWorkflow} disabled={status === "running"}>
Start Workflow
</button>
{status === "running" && <p>Workflow running... (ID: {workflowId})</p>}
{status === "completed" && <p>Workflow completed!</p>}
</div>
);
}
```
### Pattern 2: Store Workflow State in Supabase
**Track workflow progress in Lovable's built-in PostgreSQL:**
```typescript
// server/routes/workflows.ts
router.post("/api/workflows/start", async (req, res) => {
const { workflowType, input } = req.body;
const workflowId = `workflow-${Date.now()}`;
// Store in database
await supabase.from("workflows").insert({
id: workflowId,
type: workflowType,
input,
status: "pending",
created_at: new Date().toISOString()
});
// Start Resonate workflow
await resonate.beginRpc(workflowId, workflowType, input, resonate.options({
target: "poll://any@workers"
}));
res.json({ workflowId });
});
// Webhook for completion (called by external worker)
router.post("/api/workflows/:id/complete", async (req, res) => {
const { id } = req.params;
const { result } = req.body;
await supabase.from("workflows").update({
status: "completed",
result,
completed_at: new Date().toISOString()
}).eq("id", id);
res.json({ success: true });
});
```
### Pattern 3: Human-in-the-Loop with Lovable UI
**Workflow creates approval request, Lovable UI resolves it:**
```typescript
// External worker
function* approvalWorkflow(ctx: Context, orderId: string) {
const promise = yield* ctx.promise({
timeout: 24 * 60 * 60 * 1000
});
// Notify Lovable backend
yield* ctx.run(async () => {
await fetch(`${lovableBackendUrl}/api/approvals/create`, {
method: "POST",
body: JSON.stringify({
orderId,
promiseId: promise.id
})
});
});
const decision = yield* promise;
return decision;
}
// Lovable backend
router.post("/api/approvals/resolve/:promiseId", async (req, res) => {
const { promiseId } = req.params;
const { approved } = req.body;
const data = Buffer.from(JSON.stringify({ approved })).toString('base64');
await resonate.promises.resolve(promiseId, { data });
res.json({ success: true });
});
// Lovable frontend
function ApprovalButton({ promiseId }: { promiseId: string }) {
const handleApprove = async () => {
await fetch(`/api/approvals/resolve/${promiseId}`, {
method: "POST",
body: JSON.stringify({ approved: true })
});
};
return <button onClick={handleApprove}>Approve</button>;
}
```
## Deployment Strategy
### Step 1: Deploy Resonate Server
**Options:**
- Google Cloud Run (recommended for Lovable users)
- Fly.io
- Railway
- Render
**Example (Cloud Run):**
```bash
# Deploy Resonate server
docker pull resonatehq/resonate:latest
gcloud run deploy resonate-server \
--image resonatehq/resonate:latest \
--platform managed \
--region us-central1 \
--allow-unauthenticated
```
### Step 2: Deploy Resonate Workers
**Same platforms, separate service:**
```bash
# Build and deploy worker
docker build -t workers .
gcloud run deploy resonate-workers \
--image gcr.io/PROJECT/workers \
--platform managed \
--set-env-vars RESONATE_URL=https://resonate-server-xxx.run.app
```
Ver en GitHub