app (next.js) render (bun) gateway (hono/effect-ts)
├── supabase auth ├── POST /api/render ├── POST /v1/video,image,speech,music
├── chat (claude) → renderVideo ├── tsx-eval (vargai jsx) ├── provider proxy (fal, 11labs, replicate, higgsfield)
├── projects, documents, workflows├── sqlite-cache OR redis-cache ├── job queue (forkDaemon)
├── ✅ writes media ├── r2-storage (StorageProvider) ├── billing: usage table (cost_cents, billing_type)
├── ✅ writes project_media ├── zod request validation ├── auth: api_keys table (sha256 hash)
├── ✅ writes document_media ├── zero supabase dependency ├── cache: vercel kv (30d ttl)
└── calls render w/ VARG_SERVICE_KEY│ └── files: r2 upload + dedup
├── ✅ returns { url, files }
├── ✅ content-hash cache (no projectId)
├── ✅ no project/document knowledge
└── calls gateway w/ VARG_API_KEY
render knows NOTHING about projects, documents, or app's database.
consumers (agent, human, cli)
│
▼
┌──────────────────────┐
│ render │ input: { code, userId }
│ │ output: { url, files: [{ url, mediaType, metadata }] }
│ • tsx eval │
│ • mediaStorage (r2) │ ← saves all files to s3, owns them
│ • cache (key→url) │ ← prompt dedup, content-hash scoped (NOT project-scoped)
│ • sqlite or redis │ ← auto-selects via REDIS_URL env var
│ │
│ zero knowledge of: │
│ • projects │
│ • documents │
│ • billing │
│ • app's supabase │
└───────┬───────────────┘
│ VARG_API_KEY
▼
┌──────────────────────┐
│ gateway │
│ • provider proxy │ fal, 11labs, replicate, higgsfield
│ • billing │ records usage by user_id in own postgres
│ • job orchestration │
│ • own postgres │ users, api_keys, usage, jobs
└──────────────────────┘
▲
│ VARG_API_KEY
┌──────────────────────┐
│ app │
│ • supabase auth │
│ • projects/documents │ ← app links render output to projects AFTER receiving urls
│ • billing check │ ← app checks balance BEFORE calling render (gap)
│ • saves code:output │ ← app writes media, project_media, document_media
└──────────────────────┘
key principle: render returns lazy files (s3 urls). app decides what to do with them — link to project, link to document, show in UI. render doesn't care.
render request is { code, userId }. no projectId, no documentId, no supabase writes.
what was done:
render/src/api/render/route.ts— rewritten: zod validation, accepts{ code, userId, verbose? }, returns{ url, files }render/src/api/render/types.ts— NEW:RenderRequestSchema(zod),FileInfo+RenderResponse(plain interfaces)render/src/api/render/supabase-cache.ts— DELETEDrender/src/utils/supabase.ts— DELETEDrender/src/utils/upload.ts— DELETEDrender/package.json— removed@supabase/supabase-js, addedzod
PR: https://github.com/vargHQ/render/pull/2 (merged to main)
- cache =
CacheStorageinterface fromvargai/aisdk. two implementations:render/src/api/render/sqlite-cache.ts— bun:sqlite, content-hash scoped, local dev defaultrender/src/api/render/redis-cache.ts— Bun.redis, 30d TTL, production (auto-selected whenREDIS_URLset)
- mediaStorage =
StorageProviderinterface fromvargai/aisdkrender/src/api/render/r2-storage.ts— importsStorageProviderfrom sdk, removeduploadBlob
- shared utils =
render/src/api/render/cache-utils.ts—extractMediaData,fetchAsUint8Array,reconstructCachedValue(shared between sqlite and redis)
render has zero supabase dependency. cache is self-contained:
- sqlite: local file
data/cache.db, auto-creates table - redis: connects via
REDIS_URL, 30d TTL, shared across instances
what moved to app (✅ done):
mediainserts → app creates media record after receiving render urlsproject_mediainserts → app links media to projectdocument_mediainserts → app links media to document with role/sourceInfo
render_jobs table (✅ done, PR #3):
- drizzle-orm with dual driver: bun:sqlite local, postgres prod
- migrations via fly.io
release_command(runs before traffic) - records every render: status, input code hash, output url, files, duration, errors
infra provisioned:
- upstash redis on fly (
REDIS_URL) — render cache - postgres database
renderonvarg-dbcluster (DATABASE_URL) — job history
app PR: https://github.com/vargHQ/app/pull/85 (merged)
render has no billing code (already clean). gateway records usage when providers are called.
gap: app needs pre-render balance check against gateway usage data.
response contract:
// Request: POST /api/render, Authorization: Bearer <VARG_SERVICE_KEY>
{ code: string, userId: string, verbose?: boolean }
// Response:
{ url: string, files: Array<{ url: string | null, mediaType: string, metadata: { type?, model?, prompt? } }> }uses RenderResult.files from vargai sdk (not custom assets format).
no changes needed. app → render uses VARG_SERVICE_KEY. render → gateway uses VARG_API_KEY.
phase 1: decouple render internals ✅ DONE (PR #2 merged)
├── ✅ split supabase-cache.ts → sqlite-cache.ts + redis-cache.ts + cache-utils.ts
├── ✅ remove project_media / document_media writes from render
├── ✅ remove projectId from render request interface
├── ✅ add zod validation for request
└── ✅ import StorageProvider + CacheStorage from vargai/ai sdk
phase 2: render gets own db ✅ DONE
├── ✅ sqlite cache (bun:sqlite, content-hash scoped)
├── ✅ redis cache (Bun.redis, 30d TTL, auto-selected via REDIS_URL)
├── ✅ remove supabase dependency entirely
├── ✅ render_jobs table (drizzle-orm, sqlite local + postgres prod)
├── ✅ upstash redis provisioned on fly (REDIS_URL)
└── ✅ postgres db provisioned on varg-db cluster (DATABASE_URL)
phase 3: app takes ownership of linking ✅ DONE (PR #85 merged)
├── ✅ app writes media after receiving render response
├── ✅ app writes project_media after receiving render response
├── ✅ app writes document_media after receiving render response
└── ⏳ app checks billing before calling render (gap)
phase 4: (optional) gateway wraps render ⏳ NOT STARTED
├── add POST /v1/render to gateway
├── app calls gateway instead of render directly
└── gateway handles billing + auth + routing
renders/{render_id}/output.mp4 # final video
renders/{render_id}/assets/{hash}.ext # intermediate assets (images, audio)
cache/{content_hash}.ext # cached generation outputs
no more projectId in s3 paths. render doesn't know about projects.
- ✅ auth (401 on bad token)
- ✅ zod validation (400 on missing fields)
- ✅ tsx eval rejection (no
export default) - ✅ image generation via fal (flux-schnell) + r2 upload
- ✅ sqlite cache (write + hit)
- ✅ redis cache (write + hit)
- ❌ video stitching — rendi can't reach localhost minio (known dev limitation)
- ✅ app → render connection (auth, correct request shape)
- ✅ render → fal image generation works
- ✅ render → kling video generation works
- ✅ render → rendi video stitching works (3 clips)
- ✅ render returns { url, files } to app
- ✅ app links media + project_media + document_media in supabase
- ✅ video url: su.varg.ai/renders/... (production R2)
- merge app PR #85
- full e2e test (3-clip cat video, prod render → prod app, video stitched + returned)
- add
data/to render.gitignore(already there) - sdk issue #115 — improve CacheStorage interface
- app pre-render billing check (phase 4 prerequisite)
- phase 4: gateway wraps render (optional)
- render PR #2: https://github.com/vargHQ/render/pull/2 (merged — render independence)
- render PR #3: https://github.com/vargHQ/render/pull/3 (draft — render_jobs history)
- app PR: https://github.com/vargHQ/app/pull/85 (merged)
- sdk issue: vargHQ/sdk#115
- gist: https://gist.github.com/caffeinum/98ad579558a41a73bd1976fe0f3dd681