loopwind 0.24.0 → 0.24.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -6
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +22 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/tailwind.d.ts.map +1 -1
- package/dist/lib/tailwind.js +11 -5
- package/dist/lib/tailwind.js.map +1 -1
- package/package.json +1 -1
- package/plans/PLATFORM.md +1001 -0
- package/plans/PLATFORM_IMPLEMENTATION.md +2772 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
# Loopwind API Service Design
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This document outlines the architecture for a cloud-based Loopwind API service that enables:
|
|
6
|
+
- **Publishing templates** via `loopwind publish template-name`
|
|
7
|
+
- **User accounts** at loopwind.dev with authentication
|
|
8
|
+
- **Secure API endpoints** for generating images/videos from published templates
|
|
9
|
+
- **Template marketplace** for sharing and discovering templates
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Architecture
|
|
14
|
+
|
|
15
|
+
### High-Level Architecture
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
19
|
+
│ loopwind CLI │
|
|
20
|
+
│ loopwind publish / loopwind render --remote │
|
|
21
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
22
|
+
│
|
|
23
|
+
▼
|
|
24
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
25
|
+
│ API Gateway (Kong/AWS) │
|
|
26
|
+
│ Rate Limiting • Auth Validation • Request Routing │
|
|
27
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
28
|
+
│
|
|
29
|
+
┌───────────────────────┼───────────────────────┐
|
|
30
|
+
▼ ▼ ▼
|
|
31
|
+
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
|
|
32
|
+
│ Auth Service │ │ Template Service │ │ Render Service │
|
|
33
|
+
│ │ │ │ │ │
|
|
34
|
+
│ • User accounts │ │ • Template CRUD │ │ • Image rendering │
|
|
35
|
+
│ • API keys │ │ • Version control │ │ • Video rendering │
|
|
36
|
+
│ • OAuth │ │ • Validation │ │ • Job queue │
|
|
37
|
+
│ • Sessions │ │ • Search/browse │ │ • Webhooks │
|
|
38
|
+
└───────────────────┘ └───────────────────┘ └───────────────────┘
|
|
39
|
+
│ │ │
|
|
40
|
+
└───────────────────────┼───────────────────────┘
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
43
|
+
│ Data Layer │
|
|
44
|
+
│ PostgreSQL (users, templates) • Redis (cache, sessions, queue) │
|
|
45
|
+
│ S3/R2 (template files, renders) • CDN (output delivery) │
|
|
46
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Design Principles
|
|
50
|
+
|
|
51
|
+
1. **Microservices** - Independent, deployable services for auth, templates, and rendering
|
|
52
|
+
2. **WASM-first rendering** - Serverless-compatible using existing h264-mp4-encoder/Resvg
|
|
53
|
+
3. **Multi-tenancy** - Isolated user data with shared infrastructure
|
|
54
|
+
4. **Queue-based video rendering** - Long-running jobs handled asynchronously
|
|
55
|
+
5. **Edge caching** - Fast delivery of rendered assets via CDN
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Authentication System
|
|
60
|
+
|
|
61
|
+
### User Authentication
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
POST /api/auth/register
|
|
65
|
+
{
|
|
66
|
+
"email": "user@example.com",
|
|
67
|
+
"password": "securepassword",
|
|
68
|
+
"name": "John Doe"
|
|
69
|
+
}
|
|
70
|
+
→ 201 { "user": {...}, "token": "jwt..." }
|
|
71
|
+
|
|
72
|
+
POST /api/auth/login
|
|
73
|
+
{
|
|
74
|
+
"email": "user@example.com",
|
|
75
|
+
"password": "securepassword"
|
|
76
|
+
}
|
|
77
|
+
→ 200 { "user": {...}, "token": "jwt...", "refreshToken": "..." }
|
|
78
|
+
|
|
79
|
+
POST /api/auth/refresh
|
|
80
|
+
{
|
|
81
|
+
"refreshToken": "..."
|
|
82
|
+
}
|
|
83
|
+
→ 200 { "token": "jwt...", "refreshToken": "..." }
|
|
84
|
+
|
|
85
|
+
POST /api/auth/logout
|
|
86
|
+
→ 204 No Content
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### API Keys for Programmatic Access
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
POST /api/keys
|
|
93
|
+
{
|
|
94
|
+
"name": "Production App",
|
|
95
|
+
"scopes": ["render:read", "render:write", "templates:read"]
|
|
96
|
+
}
|
|
97
|
+
→ 201 {
|
|
98
|
+
"id": "key_abc123",
|
|
99
|
+
"key": "lw_live_xxxxxxxxxxxxxxxxxxxx", // Only shown once
|
|
100
|
+
"name": "Production App",
|
|
101
|
+
"scopes": [...],
|
|
102
|
+
"createdAt": "2025-01-05T..."
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
GET /api/keys
|
|
106
|
+
→ 200 [{ "id": "key_abc123", "name": "...", "lastUsed": "...", ... }]
|
|
107
|
+
|
|
108
|
+
DELETE /api/keys/:id
|
|
109
|
+
→ 204 No Content
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### CLI Authentication Flow
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# Login via browser (OAuth-style device flow)
|
|
116
|
+
$ loopwind login
|
|
117
|
+
Opening browser for authentication...
|
|
118
|
+
Waiting for confirmation...
|
|
119
|
+
✓ Logged in as user@example.com
|
|
120
|
+
|
|
121
|
+
# Token stored in ~/.loopwind/credentials
|
|
122
|
+
{
|
|
123
|
+
"token": "jwt...",
|
|
124
|
+
"refreshToken": "...",
|
|
125
|
+
"email": "user@example.com",
|
|
126
|
+
"expiresAt": "2025-01-06T..."
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Or use API key directly
|
|
130
|
+
$ export LOOPWIND_API_KEY=lw_live_xxxxxxxxxxxxxxxxxxxx
|
|
131
|
+
$ loopwind publish my-template
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Template Publishing
|
|
137
|
+
|
|
138
|
+
### Publish Command
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
loopwind publish [template-name] [options]
|
|
142
|
+
|
|
143
|
+
Options:
|
|
144
|
+
--version <version> Semantic version (default: auto-increment)
|
|
145
|
+
--description <text> Template description
|
|
146
|
+
--private Make template private (default: public)
|
|
147
|
+
--tags <tags> Comma-separated tags
|
|
148
|
+
--dry-run Validate without publishing
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Publishing Flow
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
┌─────────────┐ ┌──────────────┐ ┌────────────────┐ ┌─────────────┐
|
|
155
|
+
│ Validate │ → │ Package │ → │ Upload │ → │ Publish │
|
|
156
|
+
│ Template │ │ Files │ │ to Storage │ │ Metadata │
|
|
157
|
+
└─────────────┘ └──────────────┘ └────────────────┘ └─────────────┘
|
|
158
|
+
│ │ │ │
|
|
159
|
+
▼ ▼ ▼ ▼
|
|
160
|
+
Check meta.ts Bundle template POST files to S3 Update registry
|
|
161
|
+
Validate props Include assets Get file URLs Set version
|
|
162
|
+
Check deps Compress Store checksums Index for search
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Publish API Endpoints
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
POST /api/templates/publish
|
|
169
|
+
Authorization: Bearer <token>
|
|
170
|
+
Content-Type: multipart/form-data
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
"name": "og-card", // Required
|
|
174
|
+
"version": "1.0.0", // Semver
|
|
175
|
+
"description": "Dynamic OG images", // Optional
|
|
176
|
+
"private": false, // Default: false
|
|
177
|
+
"tags": ["og", "social", "image"], // Optional
|
|
178
|
+
"files": [ // Multipart files
|
|
179
|
+
{ "path": "template.tsx", "content": "..." },
|
|
180
|
+
{ "path": "assets/logo.png", "content": "..." }
|
|
181
|
+
],
|
|
182
|
+
"meta": { // Extracted from template
|
|
183
|
+
"type": "image",
|
|
184
|
+
"size": { "width": 1200, "height": 630 },
|
|
185
|
+
"props": { "title": "string", "subtitle": "string?" }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
→ 201 {
|
|
190
|
+
"id": "tpl_abc123",
|
|
191
|
+
"name": "og-card",
|
|
192
|
+
"version": "1.0.0",
|
|
193
|
+
"author": "username",
|
|
194
|
+
"url": "https://loopwind.dev/username/og-card",
|
|
195
|
+
"publishedAt": "2025-01-05T..."
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Template Metadata Schema
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
interface PublishedTemplate {
|
|
203
|
+
id: string; // tpl_abc123
|
|
204
|
+
name: string; // og-card
|
|
205
|
+
slug: string; // username/og-card
|
|
206
|
+
version: string; // 1.0.0
|
|
207
|
+
description: string;
|
|
208
|
+
|
|
209
|
+
author: {
|
|
210
|
+
id: string;
|
|
211
|
+
username: string;
|
|
212
|
+
avatar?: string;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
meta: {
|
|
216
|
+
type: "image" | "video";
|
|
217
|
+
size: { width: number; height: number };
|
|
218
|
+
video?: { fps: number; duration: number };
|
|
219
|
+
props: Record<string, string>;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
files: {
|
|
223
|
+
path: string;
|
|
224
|
+
url: string; // CDN URL
|
|
225
|
+
checksum: string;
|
|
226
|
+
}[];
|
|
227
|
+
|
|
228
|
+
stats: {
|
|
229
|
+
downloads: number;
|
|
230
|
+
renders: number;
|
|
231
|
+
stars: number;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
private: boolean;
|
|
235
|
+
tags: string[];
|
|
236
|
+
|
|
237
|
+
createdAt: string;
|
|
238
|
+
updatedAt: string;
|
|
239
|
+
publishedAt: string;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Rendering API
|
|
246
|
+
|
|
247
|
+
### Synchronous Rendering (Images)
|
|
248
|
+
|
|
249
|
+
For quick image renders (<5 seconds):
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
POST /api/render
|
|
253
|
+
Authorization: Bearer <token>
|
|
254
|
+
Content-Type: application/json
|
|
255
|
+
|
|
256
|
+
{
|
|
257
|
+
"template": "username/og-card", // Template slug or ID
|
|
258
|
+
"version": "latest", // Optional, default: latest
|
|
259
|
+
"props": {
|
|
260
|
+
"title": "Hello World",
|
|
261
|
+
"subtitle": "My awesome post"
|
|
262
|
+
},
|
|
263
|
+
"format": "png", // png, jpg, webp, svg
|
|
264
|
+
"scale": 2 // Optional, for retina (default: 1)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
→ 200 (binary image data)
|
|
268
|
+
Content-Type: image/png
|
|
269
|
+
X-Render-Duration: 234ms
|
|
270
|
+
X-Render-Id: rnd_xyz789
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Asynchronous Rendering (Videos)
|
|
274
|
+
|
|
275
|
+
For longer video renders:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
POST /api/render/async
|
|
279
|
+
Authorization: Bearer <token>
|
|
280
|
+
Content-Type: application/json
|
|
281
|
+
|
|
282
|
+
{
|
|
283
|
+
"template": "username/promo-video",
|
|
284
|
+
"props": {
|
|
285
|
+
"title": "Product Launch",
|
|
286
|
+
"logoUrl": "https://example.com/logo.png"
|
|
287
|
+
},
|
|
288
|
+
"format": "mp4", // mp4, gif
|
|
289
|
+
"webhook": "https://myapp.com/hooks/render-complete" // Optional
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
→ 202 {
|
|
293
|
+
"jobId": "job_abc123",
|
|
294
|
+
"status": "queued",
|
|
295
|
+
"estimatedDuration": 15000, // ms
|
|
296
|
+
"statusUrl": "/api/render/job_abc123",
|
|
297
|
+
"createdAt": "2025-01-05T..."
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Job Status Polling
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
GET /api/render/:jobId
|
|
305
|
+
Authorization: Bearer <token>
|
|
306
|
+
|
|
307
|
+
→ 200 {
|
|
308
|
+
"jobId": "job_abc123",
|
|
309
|
+
"status": "processing", // queued, processing, completed, failed
|
|
310
|
+
"progress": 0.45, // 0-1 for video
|
|
311
|
+
"currentFrame": 45,
|
|
312
|
+
"totalFrames": 100,
|
|
313
|
+
"createdAt": "2025-01-05T...",
|
|
314
|
+
"startedAt": "2025-01-05T..."
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// When completed:
|
|
318
|
+
→ 200 {
|
|
319
|
+
"jobId": "job_abc123",
|
|
320
|
+
"status": "completed",
|
|
321
|
+
"progress": 1,
|
|
322
|
+
"outputUrl": "https://cdn.loopwind.dev/renders/job_abc123.mp4",
|
|
323
|
+
"expiresAt": "2025-01-06T...", // URL expires in 24h
|
|
324
|
+
"duration": 12500, // Render time in ms
|
|
325
|
+
"completedAt": "2025-01-05T..."
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### Webhook Payload
|
|
330
|
+
|
|
331
|
+
```
|
|
332
|
+
POST https://myapp.com/hooks/render-complete
|
|
333
|
+
Content-Type: application/json
|
|
334
|
+
X-Loopwind-Signature: sha256=...
|
|
335
|
+
|
|
336
|
+
{
|
|
337
|
+
"event": "render.completed",
|
|
338
|
+
"jobId": "job_abc123",
|
|
339
|
+
"status": "completed",
|
|
340
|
+
"outputUrl": "https://cdn.loopwind.dev/renders/job_abc123.mp4",
|
|
341
|
+
"template": "username/promo-video",
|
|
342
|
+
"format": "mp4",
|
|
343
|
+
"duration": 12500,
|
|
344
|
+
"timestamp": "2025-01-05T..."
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### CLI Remote Rendering
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
# Render using cloud API (fast, no local compute)
|
|
352
|
+
$ loopwind render my-template --remote --props '{"title": "Hello"}'
|
|
353
|
+
✓ Rendered in 234ms
|
|
354
|
+
Output: https://cdn.loopwind.dev/renders/rnd_xyz789.png
|
|
355
|
+
|
|
356
|
+
# For videos (async)
|
|
357
|
+
$ loopwind render promo-video --remote --format mp4 --props '{"title": "Launch"}'
|
|
358
|
+
⠋ Rendering video... 45% (45/100 frames)
|
|
359
|
+
✓ Rendered in 12.5s
|
|
360
|
+
Output: https://cdn.loopwind.dev/renders/job_abc123.mp4
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## Template Marketplace
|
|
366
|
+
|
|
367
|
+
### Discovery Endpoints
|
|
368
|
+
|
|
369
|
+
```
|
|
370
|
+
GET /api/templates
|
|
371
|
+
?q=<search> Search query
|
|
372
|
+
&type=image|video Filter by type
|
|
373
|
+
&tags=og,social Filter by tags
|
|
374
|
+
&author=username Filter by author
|
|
375
|
+
&sort=popular|recent|downloads
|
|
376
|
+
&page=1&limit=20
|
|
377
|
+
|
|
378
|
+
→ 200 {
|
|
379
|
+
"templates": [...],
|
|
380
|
+
"total": 156,
|
|
381
|
+
"page": 1,
|
|
382
|
+
"pages": 8
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
GET /api/templates/:slug
|
|
386
|
+
→ 200 { ...PublishedTemplate }
|
|
387
|
+
|
|
388
|
+
GET /api/templates/:slug/versions
|
|
389
|
+
→ 200 [
|
|
390
|
+
{ "version": "1.0.0", "publishedAt": "...", "downloads": 50 },
|
|
391
|
+
{ "version": "0.9.0", "publishedAt": "...", "downloads": 120 }
|
|
392
|
+
]
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### User Profile & Templates
|
|
396
|
+
|
|
397
|
+
```
|
|
398
|
+
GET /api/users/:username
|
|
399
|
+
→ 200 {
|
|
400
|
+
"username": "johndoe",
|
|
401
|
+
"name": "John Doe",
|
|
402
|
+
"avatar": "https://...",
|
|
403
|
+
"bio": "Designer & developer",
|
|
404
|
+
"templates": [...],
|
|
405
|
+
"stats": { "templates": 12, "totalDownloads": 5000 }
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
GET /api/me/templates
|
|
409
|
+
Authorization: Bearer <token>
|
|
410
|
+
→ 200 [...] // All user's templates including private
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Pricing & Rate Limits
|
|
416
|
+
|
|
417
|
+
### Free Tier
|
|
418
|
+
- 100 image renders/month
|
|
419
|
+
- 10 video renders/month
|
|
420
|
+
- 5 published templates
|
|
421
|
+
- Public templates only
|
|
422
|
+
- Rate limit: 10 req/min
|
|
423
|
+
|
|
424
|
+
### Pro Tier ($19/month)
|
|
425
|
+
- 5,000 image renders/month
|
|
426
|
+
- 500 video renders/month
|
|
427
|
+
- Unlimited published templates
|
|
428
|
+
- Private templates
|
|
429
|
+
- Priority rendering
|
|
430
|
+
- Rate limit: 100 req/min
|
|
431
|
+
- Webhook support
|
|
432
|
+
|
|
433
|
+
### Business Tier ($99/month)
|
|
434
|
+
- 50,000 image renders/month
|
|
435
|
+
- 5,000 video renders/month
|
|
436
|
+
- Team accounts
|
|
437
|
+
- Custom domains
|
|
438
|
+
- SLA guarantee
|
|
439
|
+
- Rate limit: 500 req/min
|
|
440
|
+
- API analytics
|
|
441
|
+
|
|
442
|
+
### Rate Limit Headers
|
|
443
|
+
|
|
444
|
+
```
|
|
445
|
+
X-RateLimit-Limit: 100
|
|
446
|
+
X-RateLimit-Remaining: 95
|
|
447
|
+
X-RateLimit-Reset: 1704499200
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
452
|
+
## Infrastructure
|
|
453
|
+
|
|
454
|
+
### Recommended Stack (100% Cloudflare)
|
|
455
|
+
|
|
456
|
+
| Component | Technology | Why |
|
|
457
|
+
|-----------|------------|-----|
|
|
458
|
+
| API Gateway | Cloudflare Workers | Rate limiting, auth, routing - all at edge |
|
|
459
|
+
| Auth | Workers + D1 | JWT validation, session management |
|
|
460
|
+
| Database | D1 (SQLite) / Turso | Users, templates, jobs - edge-native |
|
|
461
|
+
| Cache | Workers KV | Sessions, rate limits, template cache |
|
|
462
|
+
| Queue | Durable Objects | Video job orchestration |
|
|
463
|
+
| Storage | R2 | Templates, rendered outputs |
|
|
464
|
+
| CDN | Cloudflare CDN | Automatic, integrated with R2 |
|
|
465
|
+
| Image Rendering | Workers (WASM) | Satori + Resvg, up to 5min CPU |
|
|
466
|
+
| Video Rendering | Containers | FFmpeg, unlimited duration, GPU support |
|
|
467
|
+
| Monitoring | Workers Analytics + Sentry | Built-in metrics + error tracking |
|
|
468
|
+
|
|
469
|
+
### Deployment Architecture
|
|
470
|
+
|
|
471
|
+
```
|
|
472
|
+
┌─────────────────────────┐
|
|
473
|
+
│ Cloudflare CDN │
|
|
474
|
+
│ (R2 public buckets) │
|
|
475
|
+
└───────────┬─────────────┘
|
|
476
|
+
│
|
|
477
|
+
┌────────────────────────────────────┼────────────────────────────────────┐
|
|
478
|
+
│ │ │
|
|
479
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
480
|
+
│ │ Cloudflare Workers │ │
|
|
481
|
+
│ │ │ │
|
|
482
|
+
│ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────┐ │ │
|
|
483
|
+
│ │ │ API Worker │ │ Template │ │ Image Render Worker │ │ │
|
|
484
|
+
│ │ │ │ │ Worker │ │ │ │ │
|
|
485
|
+
│ │ │ • Auth/JWT │ │ • Publish │ │ • Satori (JSX→SVG) │ │ │
|
|
486
|
+
│ │ │ • Rate limit │ │ • Validate │ │ • Resvg (SVG→PNG) │ │ │
|
|
487
|
+
│ │ │ • Routing │ │ • List/Get │ │ • Up to 5min CPU │ │ │
|
|
488
|
+
│ │ └──────────────┘ └──────────────┘ └───────────────────────┘ │ │
|
|
489
|
+
│ │ │ │
|
|
490
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
491
|
+
│ │ │
|
|
492
|
+
│ │ getContainer(env.RENDERER, jobId) │
|
|
493
|
+
│ ▼ │
|
|
494
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
495
|
+
│ │ Cloudflare Containers │ │
|
|
496
|
+
│ │ │ │
|
|
497
|
+
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
|
|
498
|
+
│ │ │ job_abc │ │ job_def │ │ job_ghi │ ... │ │
|
|
499
|
+
│ │ │ Container │ │ Container │ │ Container │ │ │
|
|
500
|
+
│ │ │ │ │ │ │ │ │ │
|
|
501
|
+
│ │ │ • FFmpeg │ │ • FFmpeg │ │ • FFmpeg │ │ │
|
|
502
|
+
│ │ │ • Node.js │ │ • Node.js │ │ • Node.js │ │ │
|
|
503
|
+
│ │ │ • Loopwind │ │ • Loopwind │ │ • Loopwind │ │ │
|
|
504
|
+
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
|
|
505
|
+
│ │ │ │ │ │ │
|
|
506
|
+
│ │ └─────────────────┼─────────────────┘ │ │
|
|
507
|
+
│ │ ▼ │ │
|
|
508
|
+
│ │ Durable Object (Job Orchestrator) │ │
|
|
509
|
+
│ │ • Track progress per job │ │
|
|
510
|
+
│ │ • Manage container lifecycle │ │
|
|
511
|
+
│ │ • Handle webhooks │ │
|
|
512
|
+
│ │ │ │
|
|
513
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
514
|
+
│ │ │
|
|
515
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
516
|
+
│ │ Data Layer │ │
|
|
517
|
+
│ │ │ │
|
|
518
|
+
│ │ D1 (SQLite) KV R2 │ │
|
|
519
|
+
│ │ • Users • Sessions • Templates │ │
|
|
520
|
+
│ │ • Templates • Rate limits • Rendered videos │ │
|
|
521
|
+
│ │ • Jobs • Template cache • Assets │ │
|
|
522
|
+
│ │ • Usage • Font cache │ │
|
|
523
|
+
│ │ │ │
|
|
524
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
525
|
+
│ │
|
|
526
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Cloudflare Containers for Video Rendering
|
|
530
|
+
|
|
531
|
+
[Cloudflare Containers](https://blog.cloudflare.com/cloudflare-containers-coming-2025/) (launched June 2025) enable **full video rendering on Cloudflare**:
|
|
532
|
+
|
|
533
|
+
#### Key Features
|
|
534
|
+
- **Docker containers** - Run FFmpeg, Node.js, any runtime
|
|
535
|
+
- **No time limits** - Long-running video encodes supported
|
|
536
|
+
- **On-demand scaling** - Containers boot per job, sleep when idle
|
|
537
|
+
- **GPU support** - Hardware-accelerated encoding available
|
|
538
|
+
- **Global deployment** - Containers run near users automatically
|
|
539
|
+
- **Pay per use** - Only charged for active container time
|
|
540
|
+
|
|
541
|
+
#### Container Architecture
|
|
542
|
+
|
|
543
|
+
Each video job gets its own container instance:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// Worker routes video jobs to containers
|
|
547
|
+
import { getContainer } from "cloudflare:container";
|
|
548
|
+
|
|
549
|
+
export default {
|
|
550
|
+
async fetch(request: Request, env: Env) {
|
|
551
|
+
const { jobId, template, props, format } = await request.json();
|
|
552
|
+
|
|
553
|
+
// Each unique jobId = separate container instance
|
|
554
|
+
// Containers auto-scale horizontally
|
|
555
|
+
const container = await getContainer(env.VIDEO_RENDERER, jobId);
|
|
556
|
+
|
|
557
|
+
return container.fetch("/render", {
|
|
558
|
+
method: "POST",
|
|
559
|
+
body: JSON.stringify({ template, props, format })
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
#### Container Dockerfile
|
|
566
|
+
|
|
567
|
+
```dockerfile
|
|
568
|
+
FROM node:20-slim
|
|
569
|
+
|
|
570
|
+
# Install FFmpeg for video encoding
|
|
571
|
+
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
|
572
|
+
|
|
573
|
+
# Install loopwind
|
|
574
|
+
WORKDIR /app
|
|
575
|
+
COPY package*.json ./
|
|
576
|
+
RUN npm ci --production
|
|
577
|
+
COPY . .
|
|
578
|
+
|
|
579
|
+
# Render server listens for jobs
|
|
580
|
+
EXPOSE 8080
|
|
581
|
+
CMD ["node", "render-server.js"]
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
#### Scaling Patterns
|
|
585
|
+
|
|
586
|
+
**Pattern 1: Job-per-Container (Recommended)**
|
|
587
|
+
```
|
|
588
|
+
POST /render/video { jobId: "job_abc" }
|
|
589
|
+
→ Container "job_abc" boots
|
|
590
|
+
→ Renders video
|
|
591
|
+
→ Uploads to R2
|
|
592
|
+
→ Container sleeps after 60s idle
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Pattern 2: Pooled Workers**
|
|
596
|
+
```typescript
|
|
597
|
+
// Distribute across N warm containers using consistent hashing
|
|
598
|
+
const POOL_SIZE = 10;
|
|
599
|
+
const workerId = `renderer-${hash(jobId) % POOL_SIZE}`;
|
|
600
|
+
const container = await getContainer(env.VIDEO_RENDERER, workerId);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
#### Durable Object Orchestration
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// Durable Object manages container lifecycle and job state
|
|
607
|
+
export class VideoJobOrchestrator extends DurableObject {
|
|
608
|
+
async startJob(jobId: string, params: RenderParams) {
|
|
609
|
+
// Store job state
|
|
610
|
+
await this.ctx.storage.put(`job:${jobId}`, {
|
|
611
|
+
status: "processing",
|
|
612
|
+
progress: 0,
|
|
613
|
+
startedAt: Date.now()
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
// Get or create container for this job
|
|
617
|
+
const container = await getContainer(this.env.VIDEO_RENDERER, jobId);
|
|
618
|
+
|
|
619
|
+
// Start render (non-blocking)
|
|
620
|
+
container.fetch("/render", {
|
|
621
|
+
method: "POST",
|
|
622
|
+
body: JSON.stringify(params)
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
return { jobId, status: "processing" };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async updateProgress(jobId: string, progress: number) {
|
|
629
|
+
const job = await this.ctx.storage.get(`job:${jobId}`);
|
|
630
|
+
job.progress = progress;
|
|
631
|
+
await this.ctx.storage.put(`job:${jobId}`, job);
|
|
632
|
+
|
|
633
|
+
// Notify via WebSocket if client connected
|
|
634
|
+
this.broadcast({ type: "progress", jobId, progress });
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async completeJob(jobId: string, outputUrl: string) {
|
|
638
|
+
await this.ctx.storage.put(`job:${jobId}`, {
|
|
639
|
+
status: "completed",
|
|
640
|
+
progress: 1,
|
|
641
|
+
outputUrl,
|
|
642
|
+
completedAt: Date.now()
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Fire webhook if configured
|
|
646
|
+
await this.fireWebhook(jobId, outputUrl);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Workers for Image Rendering
|
|
652
|
+
|
|
653
|
+
Cloudflare Workers handle all image rendering with [up to 5 minutes CPU time](https://developers.cloudflare.com/changelog/2025-03-25-higher-cpu-limits/):
|
|
654
|
+
|
|
655
|
+
```toml
|
|
656
|
+
# wrangler.toml
|
|
657
|
+
name = "loopwind-api"
|
|
658
|
+
main = "src/worker.ts"
|
|
659
|
+
compatibility_date = "2025-01-01"
|
|
660
|
+
|
|
661
|
+
[limits]
|
|
662
|
+
cpu_ms = 300000 # 5 minutes - enough for complex images
|
|
663
|
+
|
|
664
|
+
[[d1_databases]]
|
|
665
|
+
binding = "DB"
|
|
666
|
+
database_name = "loopwind"
|
|
667
|
+
|
|
668
|
+
[[r2_buckets]]
|
|
669
|
+
binding = "STORAGE"
|
|
670
|
+
bucket_name = "loopwind-assets"
|
|
671
|
+
|
|
672
|
+
[[kv_namespaces]]
|
|
673
|
+
binding = "CACHE"
|
|
674
|
+
id = "xxx"
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Rendering Decision Flow
|
|
678
|
+
|
|
679
|
+
```
|
|
680
|
+
┌─────────────────┐
|
|
681
|
+
│ Render Request │
|
|
682
|
+
└────────┬────────┘
|
|
683
|
+
│
|
|
684
|
+
▼
|
|
685
|
+
┌─────────────────┐
|
|
686
|
+
│ Image or Video?│
|
|
687
|
+
└────────┬────────┘
|
|
688
|
+
│
|
|
689
|
+
┌──────────────┴──────────────┐
|
|
690
|
+
│ │
|
|
691
|
+
▼ ▼
|
|
692
|
+
┌────────────────┐ ┌────────────────┐
|
|
693
|
+
│ IMAGE │ │ VIDEO │
|
|
694
|
+
└────────┬───────┘ └────────┬───────┘
|
|
695
|
+
│ │
|
|
696
|
+
▼ ▼
|
|
697
|
+
┌────────────────┐ ┌────────────────┐
|
|
698
|
+
│ Worker │ │ Container │
|
|
699
|
+
│ │ │ │
|
|
700
|
+
│ • Satori→SVG │ │ • FFmpeg │
|
|
701
|
+
│ • Resvg→PNG │ │ • Full Node.js │
|
|
702
|
+
│ • <5min CPU │ │ • No time limit│
|
|
703
|
+
│ • 128MB RAM │ │ • Multi-core │
|
|
704
|
+
└────────┬───────┘ └────────┬───────┘
|
|
705
|
+
│ │
|
|
706
|
+
▼ ▼
|
|
707
|
+
┌────────────────┐ ┌────────────────┐
|
|
708
|
+
│ Sync Response │ │ Async Job ID │
|
|
709
|
+
│ (binary data) │ │ (poll/webhook) │
|
|
710
|
+
└────────────────┘ └────────────────┘
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Cost Optimization
|
|
714
|
+
|
|
715
|
+
| Workload | Platform | Estimated Cost |
|
|
716
|
+
|----------|----------|----------------|
|
|
717
|
+
| Image render | Worker | ~$0.0001 per render |
|
|
718
|
+
| Short video (<30s) | Worker (WASM) | ~$0.001 per render |
|
|
719
|
+
| Long video (1-5min) | Container | ~$0.01-0.05 per render |
|
|
720
|
+
| GPU video | Container + GPU | ~$0.10-0.50 per render |
|
|
721
|
+
|
|
722
|
+
**Tips:**
|
|
723
|
+
- Use Workers for images (cheapest, fastest)
|
|
724
|
+
- Use WASM encoding in Workers for short videos when possible
|
|
725
|
+
- Containers auto-sleep after idle timeout (default 60s)
|
|
726
|
+
- Cache fonts/assets in KV to reduce cold start time
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## CLI Implementation
|
|
731
|
+
|
|
732
|
+
### New Commands
|
|
733
|
+
|
|
734
|
+
```bash
|
|
735
|
+
# Authentication
|
|
736
|
+
loopwind login # Browser-based OAuth flow
|
|
737
|
+
loopwind logout # Clear credentials
|
|
738
|
+
loopwind whoami # Show current user
|
|
739
|
+
|
|
740
|
+
# Publishing
|
|
741
|
+
loopwind publish [template] # Publish template to registry
|
|
742
|
+
--version <ver> # Semantic version
|
|
743
|
+
--private # Private template
|
|
744
|
+
--dry-run # Validate only
|
|
745
|
+
|
|
746
|
+
# Remote rendering
|
|
747
|
+
loopwind render <template> # Render (local by default)
|
|
748
|
+
--remote # Use cloud API
|
|
749
|
+
--props <json> # Template props
|
|
750
|
+
--format <fmt> # Output format
|
|
751
|
+
--out <path> # Save locally
|
|
752
|
+
|
|
753
|
+
# Account management
|
|
754
|
+
loopwind keys list # List API keys
|
|
755
|
+
loopwind keys create # Create new API key
|
|
756
|
+
loopwind keys revoke <id> # Revoke API key
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Credential Storage
|
|
760
|
+
|
|
761
|
+
```
|
|
762
|
+
~/.loopwind/
|
|
763
|
+
├── credentials.json # Auth tokens
|
|
764
|
+
├── config.json # User preferences
|
|
765
|
+
└── keys/ # Cached API keys
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
```json
|
|
769
|
+
// ~/.loopwind/credentials.json
|
|
770
|
+
{
|
|
771
|
+
"token": "eyJhbG...",
|
|
772
|
+
"refreshToken": "eyJhbG...",
|
|
773
|
+
"expiresAt": "2025-01-06T12:00:00Z",
|
|
774
|
+
"user": {
|
|
775
|
+
"id": "usr_abc123",
|
|
776
|
+
"email": "user@example.com",
|
|
777
|
+
"username": "johndoe"
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## Security Considerations
|
|
785
|
+
|
|
786
|
+
### API Security
|
|
787
|
+
|
|
788
|
+
1. **Authentication**
|
|
789
|
+
- JWT tokens with short expiry (15min)
|
|
790
|
+
- Refresh tokens stored securely (HttpOnly cookies on web)
|
|
791
|
+
- API keys hashed with bcrypt before storage
|
|
792
|
+
|
|
793
|
+
2. **Authorization**
|
|
794
|
+
- Scoped API keys (render:read, templates:write, etc.)
|
|
795
|
+
- Private template access control
|
|
796
|
+
- Rate limiting per API key/user
|
|
797
|
+
|
|
798
|
+
3. **Input Validation**
|
|
799
|
+
- Template code sandboxed (no network access, no fs access)
|
|
800
|
+
- Props validated against schema
|
|
801
|
+
- File size limits (10MB per template)
|
|
802
|
+
|
|
803
|
+
4. **Output Security**
|
|
804
|
+
- Signed URLs with expiration
|
|
805
|
+
- CORS configuration
|
|
806
|
+
- Content-Security-Policy headers
|
|
807
|
+
|
|
808
|
+
### Template Sandboxing
|
|
809
|
+
|
|
810
|
+
Templates run in isolated environment:
|
|
811
|
+
|
|
812
|
+
```typescript
|
|
813
|
+
// Allowed in templates
|
|
814
|
+
- React JSX rendering
|
|
815
|
+
- Math operations
|
|
816
|
+
- String manipulation
|
|
817
|
+
- Built-in helpers (tw, qr, image, path)
|
|
818
|
+
|
|
819
|
+
// NOT allowed
|
|
820
|
+
- fetch() / network requests
|
|
821
|
+
- fs / file system access
|
|
822
|
+
- eval() / dynamic code
|
|
823
|
+
- process / environment access
|
|
824
|
+
- require() / import() of external modules
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
---
|
|
828
|
+
|
|
829
|
+
## Database Schema
|
|
830
|
+
|
|
831
|
+
```sql
|
|
832
|
+
-- Users
|
|
833
|
+
CREATE TABLE users (
|
|
834
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
835
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
836
|
+
username VARCHAR(50) UNIQUE NOT NULL,
|
|
837
|
+
password_hash VARCHAR(255),
|
|
838
|
+
name VARCHAR(255),
|
|
839
|
+
avatar_url VARCHAR(500),
|
|
840
|
+
plan VARCHAR(20) DEFAULT 'free',
|
|
841
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
842
|
+
updated_at TIMESTAMP DEFAULT NOW()
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
-- API Keys
|
|
846
|
+
CREATE TABLE api_keys (
|
|
847
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
848
|
+
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
849
|
+
name VARCHAR(100) NOT NULL,
|
|
850
|
+
key_hash VARCHAR(255) NOT NULL,
|
|
851
|
+
key_prefix VARCHAR(12) NOT NULL, -- lw_live_xxxx for display
|
|
852
|
+
scopes TEXT[] DEFAULT '{}',
|
|
853
|
+
last_used_at TIMESTAMP,
|
|
854
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
-- Templates
|
|
858
|
+
CREATE TABLE templates (
|
|
859
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
860
|
+
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
|
861
|
+
name VARCHAR(100) NOT NULL,
|
|
862
|
+
slug VARCHAR(150) NOT NULL, -- username/template-name
|
|
863
|
+
description TEXT,
|
|
864
|
+
type VARCHAR(20) NOT NULL, -- image, video
|
|
865
|
+
meta JSONB NOT NULL,
|
|
866
|
+
private BOOLEAN DEFAULT FALSE,
|
|
867
|
+
tags TEXT[] DEFAULT '{}',
|
|
868
|
+
downloads INTEGER DEFAULT 0,
|
|
869
|
+
renders INTEGER DEFAULT 0,
|
|
870
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
871
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
872
|
+
UNIQUE(user_id, name)
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
-- Template Versions
|
|
876
|
+
CREATE TABLE template_versions (
|
|
877
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
878
|
+
template_id UUID REFERENCES templates(id) ON DELETE CASCADE,
|
|
879
|
+
version VARCHAR(20) NOT NULL,
|
|
880
|
+
files JSONB NOT NULL, -- [{path, url, checksum}]
|
|
881
|
+
published_at TIMESTAMP DEFAULT NOW(),
|
|
882
|
+
UNIQUE(template_id, version)
|
|
883
|
+
);
|
|
884
|
+
|
|
885
|
+
-- Render Jobs
|
|
886
|
+
CREATE TABLE render_jobs (
|
|
887
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
888
|
+
user_id UUID REFERENCES users(id),
|
|
889
|
+
template_id UUID REFERENCES templates(id),
|
|
890
|
+
status VARCHAR(20) DEFAULT 'queued',
|
|
891
|
+
props JSONB,
|
|
892
|
+
format VARCHAR(10),
|
|
893
|
+
output_url VARCHAR(500),
|
|
894
|
+
error TEXT,
|
|
895
|
+
progress FLOAT DEFAULT 0,
|
|
896
|
+
started_at TIMESTAMP,
|
|
897
|
+
completed_at TIMESTAMP,
|
|
898
|
+
created_at TIMESTAMP DEFAULT NOW()
|
|
899
|
+
);
|
|
900
|
+
|
|
901
|
+
-- Usage tracking
|
|
902
|
+
CREATE TABLE usage (
|
|
903
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
904
|
+
user_id UUID REFERENCES users(id),
|
|
905
|
+
type VARCHAR(20) NOT NULL, -- image_render, video_render, publish
|
|
906
|
+
count INTEGER DEFAULT 1,
|
|
907
|
+
month DATE NOT NULL, -- First of month
|
|
908
|
+
UNIQUE(user_id, type, month)
|
|
909
|
+
);
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
---
|
|
913
|
+
|
|
914
|
+
## Implementation Phases
|
|
915
|
+
|
|
916
|
+
### Phase 1: Authentication & Publishing
|
|
917
|
+
- User registration/login
|
|
918
|
+
- API key management
|
|
919
|
+
- `loopwind login/logout/whoami` commands
|
|
920
|
+
- `loopwind publish` command
|
|
921
|
+
- Template storage in R2
|
|
922
|
+
- Basic registry API
|
|
923
|
+
|
|
924
|
+
### Phase 2: Image Rendering API
|
|
925
|
+
- Synchronous image rendering endpoint
|
|
926
|
+
- `loopwind render --remote` for images
|
|
927
|
+
- Rate limiting
|
|
928
|
+
- Usage tracking
|
|
929
|
+
|
|
930
|
+
### Phase 3: Video Rendering & Queue
|
|
931
|
+
- Async video rendering with job queue
|
|
932
|
+
- Webhook notifications
|
|
933
|
+
- Progress tracking
|
|
934
|
+
- `loopwind render --remote` for videos
|
|
935
|
+
|
|
936
|
+
### Phase 4: Marketplace & Discovery
|
|
937
|
+
- Template search and browse
|
|
938
|
+
- User profiles
|
|
939
|
+
- Download/star tracking
|
|
940
|
+
- loopwind.dev website updates
|
|
941
|
+
|
|
942
|
+
### Phase 5: Billing & Pro Features
|
|
943
|
+
- Stripe integration
|
|
944
|
+
- Usage-based billing
|
|
945
|
+
- Private templates
|
|
946
|
+
- Team accounts
|
|
947
|
+
|
|
948
|
+
---
|
|
949
|
+
|
|
950
|
+
## API Client SDK
|
|
951
|
+
|
|
952
|
+
For easy integration, provide official SDKs:
|
|
953
|
+
|
|
954
|
+
```typescript
|
|
955
|
+
// JavaScript/TypeScript
|
|
956
|
+
import { Loopwind } from '@loopwind/sdk';
|
|
957
|
+
|
|
958
|
+
const lw = new Loopwind({ apiKey: 'lw_live_xxx' });
|
|
959
|
+
|
|
960
|
+
// Render image
|
|
961
|
+
const image = await lw.render('username/og-card', {
|
|
962
|
+
props: { title: 'Hello World' },
|
|
963
|
+
format: 'png'
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
// Render video (async)
|
|
967
|
+
const job = await lw.renderVideo('username/promo', {
|
|
968
|
+
props: { title: 'Launch' },
|
|
969
|
+
format: 'mp4'
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
// Poll for completion
|
|
973
|
+
const result = await job.wait();
|
|
974
|
+
console.log(result.outputUrl);
|
|
975
|
+
|
|
976
|
+
// Or use webhook
|
|
977
|
+
await lw.renderVideo('username/promo', {
|
|
978
|
+
props: { title: 'Launch' },
|
|
979
|
+
webhook: 'https://myapp.com/hooks/render'
|
|
980
|
+
});
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## References
|
|
986
|
+
|
|
987
|
+
### Cloudflare Platform
|
|
988
|
+
- [Cloudflare Containers Announcement](https://blog.cloudflare.com/cloudflare-containers-coming-2025/) - Containers coming June 2025
|
|
989
|
+
- [Cloudflare Containers Architecture](https://developers.cloudflare.com/containers/architecture/) - How containers work with Workers/DOs
|
|
990
|
+
- [Container Platform with GPUs](https://blog.cloudflare.com/container-platform-preview/) - GPU support preview
|
|
991
|
+
- [Workers 5-Minute CPU Limit](https://developers.cloudflare.com/changelog/2025-03-25-higher-cpu-limits/) - Extended CPU time for Workers
|
|
992
|
+
- [Workers Limits](https://developers.cloudflare.com/workers/platform/limits/) - Full limits documentation
|
|
993
|
+
- [Durable Objects](https://developers.cloudflare.com/durable-objects/) - Stateful coordination
|
|
994
|
+
|
|
995
|
+
### Architecture & Design
|
|
996
|
+
- [Eden AI - Video Generation APIs](https://www.edenai.co/post/best-ai-video-generation-apis-in-2025)
|
|
997
|
+
- [SaaS Architecture Patterns](https://www.mindinventory.com/blog/software-architecture-patterns/)
|
|
998
|
+
- [API Architecture Best Practices](https://www.catchpoint.com/api-monitoring-tools/api-architecture)
|
|
999
|
+
- [Vercel Marketplace API](https://vercel.com/docs/integrations/create-integration/marketplace-api)
|
|
1000
|
+
- [Sharetribe Marketplace API](https://www.sharetribe.com/docs/concepts/api-sdk/marketplace-api-integration-api/)
|
|
1001
|
+
- [SaaS Architecture Guide 2025](https://www.decipherzone.com/blog-detail/saas-architecture-cto-guide)
|