Shipping Glimmer: A Solo Dev Post-Mortem
2026-02-26 · Jazz Lien
The Product
Glimmer turns photos into memorial videos using AI. Upload a photo of grandma, get back a 5-second video of her smiling, blinking, breathing. Add music, titles, export.
I shipped it in 6 weeks, solo. Here's what I learned.
Edge Runtime: Beautiful Until It Isn't
I chose Cloudflare Pages for the edge deployment — fast globally, generous free tier, simple deploys. But Edge Runtime has rules:
No background tasks. Workers kill your code ~30 seconds after the response. You can't fire-and-forget. My video generation takes 2-3 minutes, so I had to redesign:
Client polls /api/status/[id] every 5 seconds
→ Each request checks external AI API once
→ Updates KV with latest status
→ Returns to client
Everything is request-scoped. The client drives the polling, not the server.
No Node.js crypto. crypto.createHmac doesn't exist. Payment webhook verification had to use Web Crypto API:
const key = await crypto.subtle.importKey(
'raw',
new TextEncoder().encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, payload);
No filesystem. Obvious in hindsight, but my local dev relied on temp files. Replaced with KV (metadata) and R2 (video blobs).
KV + R2: The Storage Duo
Cloudflare KV is key-value with 24h TTL option. Perfect for job tracking:
await kvPut(`job:${id}`, JSON.stringify(jobData), { expirationTtl: 86400 });
But KV has a 25MB value limit. Videos are bigger. Enter R2:
await r2.put(`videos/${jobId}/0.mp4`, videoBlob);
R2 objects persist forever (or until you delete). I archive completed videos to R2 when the AI job finishes. Even after KV records expire, videos remain accessible.
Backward compatibility trick: Old jobs stored CDN URLs directly. New jobs store R2 keys. Detection:
if (videoUrl.startsWith('http')) {
// Legacy CDN URL — use directly
} else {
// R2 key — route through proxy
return `/api/proxy-video?jobId=${jobId}`;
}
Taiwan Payments: ECPay Over Stripe
Stripe is uncommon for Taiwan consumers. ECPay (綠界) is the standard — supports credit card, ATM transfer, convenience store codes, LINE Pay.
Registration: Start as individual. Upgrade to 行號 when revenue hits NT$240K/year.
Integration: REST API with form-encoded POST. No SDK needed (good for Edge):
const response = await fetch('https://payment.ecpay.com.tw/Cashier/AioCheckOut/V5', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(params)
});
Webhook verification: ECPay signs with CheckMacValue (SHA256). Verify before trusting:
const sorted = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
const raw = `HashKey=${key}&${sorted}&HashIV=${iv}`;
const expected = sha256(encodeURIComponent(raw).toLowerCase());
Credit System: Pay-Per-Video
Subscriptions are wrong for event-driven products. Memorial videos happen once. Weddings happen once. Users would churn immediately.
Pay-per-video with credits that never expire:
- Free: 1 video per email (prove value)
- Single: NT$499 (1 credit)
- 5-pack: NT$1,999 (NT$400/ea)
- Enterprise: Contact sales
Deduct credits AFTER API success:
await checkCredits(email); // 402 if insufficient
const result = await createVideo(); // External API call
await useCredit(email, 1); // Only after success
If the AI provider fails, user keeps their credit.
Multi-Provider Video Generation
I integrated 4 AI video providers: Google Veo, BytePlus Seedance, Kling AI, and a backup. Each has different:
- API formats
- Status field names (
status vs task_status)
- Completion values (
succeeded vs success vs completed)
Normalize at the boundary:
function checkVideoTaskStatus(job): TaskCheckResult {
switch (job.provider) {
case 'veo': return normalizeVeoStatus(job);
case 'byteplus': return normalizeBytePlusStatus(job);
// ...
}
}
Callers never see provider quirks.
What I'd Do Differently
Start with R2 from day one. Migrating from CDN URLs to R2 keys mid-project created backward-compat complexity.
Add Sentry earlier. Production errors were invisible for weeks. I only found bugs when users complained.
Build the admin panel first. Spent too much time SSH-ing into KV to debug. A simple admin UI saves hours.
Shipping Is The Goal
Glimmer isn't perfect. The landing page could be better. The editor has rough edges. But it works, it's live, and it accepts money.
Shipping beats polishing. Every time.
Glimmer is live at glimmer.video. First video is free.