Save and organize links with AI-powered summaries. Full-stack TypeScript on Cloudflare Workers.
- Link saving with automatic metadata extraction (title, description, favicon)
- AI summaries powered by OpenRouter (Gemini 2.5 Flash)
- Real-time sync across devices via LiveStore + WebSocket
- Telegram bot for saving links on the go
- AI chat for asking questions about your saved links
- Multi-workspace support with invite system
- Admin panel with usage analytics
- Frontend: React 19, Vite, TailwindCSS 4, TanStack Router
- Backend: Cloudflare Workers, Hono.js, D1 (SQLite), Drizzle ORM
- Real-time: LiveStore with Durable Objects
- AI: Vercel AI SDK + OpenRouter
- Auth: Better Auth + Google OAuth
Click the Deploy to Cloudflare button above. It will:
- Fork the repo to your GitHub account
- Provision all Cloudflare resources (D1, KV, Durable Objects)
- Prompt you for required secrets
- Deploy and set up CI/CD
After deploying, you'll need to:
- Set
BETTER_AUTH_URLto your Worker URL (e.g.https://cloudstash.your-subdomain.workers.dev) - Configure Google OAuth redirect URI (see Google OAuth Setup)
- Bootstrap the first admin (see First Admin Setup)
# Install dependencies
bun install
# Set up environment
cp .dev.vars.example .dev.vars
# Edit .dev.vars with your values (see Configuration below)
# Run database migrations
bun run db:migrate:local
# Start dev server
bun devThe app runs at http://localhost:3000.
| Variable | Description |
|---|---|
GOOGLE_CLIENT_ID |
Google OAuth Client ID (Cloud Console) |
GOOGLE_CLIENT_SECRET |
Google OAuth Client Secret |
BETTER_AUTH_SECRET |
Random string (32+ chars). Generate with: openssl rand -hex 32 |
BETTER_AUTH_URL |
Base URL (http://localhost:3000 for local, your Worker URL for production) |
| Variable | Description |
|---|---|
GOOGLE_BASE_URL |
Google OAuth base URL (default: https://accounts.google.com). Set to emulator URL for local dev |
OPENROUTER_API_KEY |
OpenRouter API key for AI chat and summaries |
RESEND_API_KEY |
Resend API key for email notifications |
EMAIL_FROM |
Custom sender address (default: CloudStash <noreply@cloudstash.dev>) |
TELEGRAM_BOT_TOKEN |
Telegram bot token from @BotFather |
TELEGRAM_WEBHOOK_SECRET |
Random string for Telegram webhook validation |
CF_ACCOUNT_ID |
Cloudflare account ID (for observability scripts) |
CF_ANALYTICS_TOKEN |
Cloudflare analytics token (for DO metrics) |
Local: Set in .dev.vars (copy from .dev.vars.example).
Production: Set via bunx wrangler secret put VARIABLE_NAME.
After the first user signs up, bootstrap admin access:
-- Via Cloudflare Dashboard > D1 > Console, or:
-- Local
wrangler d1 execute DB --local --command \
"UPDATE user SET approved = 1, role = 'admin' WHERE email = 'your@email.com'"
-- Production
wrangler d1 execute DB --remote --command \
"UPDATE user SET approved = 1, role = 'admin' WHERE email = 'your@email.com'"After this, the admin can approve other users through the UI.
- Go to Google Cloud Console
- Create a new project (or select existing)
- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Select Web application
- Add authorized redirect URIs:
- Local:
http://localhost:3000/api/auth/oauth2/callback/google - Production:
https://your-worker.workers.dev/api/auth/oauth2/callback/google
- Local:
For local development without real Google credentials, use emulate.dev to run a local Google OAuth emulator:
# Terminal 1: Start the Google OAuth emulator
bun run dev:emulate
# Terminal 2: Start the dev server
bun devSet GOOGLE_BASE_URL=http://localhost:4000 in .dev.vars to point auth at the emulator. Remove it to use real Google OAuth.
After signing in as admin@cloudstash.test, promote to admin:
bun run dev:make-admin- Message @BotFather, send
/newbot, save the token - Set
TELEGRAM_BOT_TOKENandTELEGRAM_WEBHOOK_SECRETin your env - Register the webhook:
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
-H "Content-Type: application/json" \
-d '{"url": "https://YOUR_WORKER_URL/api/telegram", "secret_token": "YOUR_WEBHOOK_SECRET"}'- Optionally set bot commands via @BotFather:
/setcommands
start - Show help
help - Show help
connect - Connect with API key
disconnect - Disconnect account
bun dev # Vite+ dev server (port 3000)
bun run dev:infra # Auth emulator, tunnel, dashboard, raycast (separate terminal)
bun run build # Production build
bun test # Run all tests
bun run test:unit # Unit tests only
bun run test:e2e # E2E tests
bun run typecheck # Type checking
bun run check # Lint + format (Vite+) + Effect diagnostics
bun run fix # Auto-fix lint issuesbun run db:generate # Generate new migration
bun run db:migrate:local # Apply migrations locallybun run deploy # Production deploy (migrations + worker)
bun run deploy:staging # Staging deployArchitecture diagrams are in docs/diagrams/ as .excalidraw files. Open them with the Excalidraw Obsidian plugin or at excalidraw.com.
src/
cf-worker/ # Cloudflare Worker backend
admin/ # Admin API endpoints
auth/ # Better Auth + Google OAuth
chat-agent/ # AI chat Durable Object
db/ # Drizzle ORM schema + migrations
email/ # React Email templates + Resend
ingest/ # Link ingestion + metadata extraction
invites/ # Workspace invite system
link-processor/ # Link processing Durable Object
org/ # Organization/workspace API
sync/ # LiveStore sync Durable Object
telegram/ # Telegram bot integration
livestore/ # LiveStore schema, events, queries
routes/ # TanStack Router pages
_authed/ # Authenticated routes
components/ # React components
hooks/ # React hooks
stores/ # Zustand stores
| Resource | Purpose |
|---|---|
| D1 Database | Users, sessions, organizations, invites |
| SyncBackendDO | LiveStore real-time sync per workspace |
| LinkProcessorDO | Async link processing + AI summaries |
| ChatAgentDO | AI chat agent per workspace |
| KV Namespace | Telegram user connections |
| Workers AI | Fallback AI for link processing |
| Rate Limiting | 30 req/min per IP on sync/auth endpoints |
| Analytics Engine | Per-user usage tracking |
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes
- Run checks:
bun run check && bun run typecheck && bun test - Submit a pull request
Uses bun (not npm) and oxlint/oxfmt via Vite+ (not eslint).
MIT