A full-stack, production-ready blogging platform β write, publish, and discover thoughtful articles. Built with a serverless-first architecture powered by Hono on Cloudflare Workers and a React + Vite frontend.
- Browse and read all published articles
- Like and bookmark posts
- Leave comments on posts
- Rich markdown editor to write and preview articles
- Pick a cover image directly from Unsplash
- Submit posts for admin review before publishing
- Manage your own posts (drafts, under review, published)
- Delete your posts
- Email + OTP signup verification (via Resend)
- Access tokens (JWT) + Refresh token rotation (stored in
httpOnlycookies) - Sign out from current session or all sessions
- Review queue for submitted posts (approve / reject with a reason)
- Ban / unban users (instantly enforced at the edge via Cloudflare KV)
- Promote users to admin
- View all registered users
- AI-powered content moderation using Groq to flag potentially harmful content before human review; automated emails notify authors on flag / approval / rejection
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Client β
β React 19 + Vite + Tailwind CSS v4 β
βββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β HTTPS
βββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββ
β Cloudflare Workers (Edge) β
β Hono framework Β· TypeScript Β· workerd β
β β
β βββββββββββββββββββ ββββββββββββββββββββββββββββββββ β
β β Auth Routes β β Blog / Admin Routes β β
β β JWT + Cookies β β CRUD Β· Likes Β· Bookmarks β β
β ββββββββββ¬βββββββββ ββββββββββββββββ¬ββββββββββββββββ β
β β β β
β ββββββββββΌββββββββββββββββββββββββββββΌββββββββββββββββ β
β β Cloudflare KV β β
β β INK_FOLD_BANNED_USERS (edge ban enforcement) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββ¬ββββββββββββββββββ¬βββββββββββββββββββββ
β β
ββββββββββββΌβββββββ βββββββββΌβββββββββββ
β Prisma Accel. β β Upstash Redis β
β (conn pooling) β β (rate limiting) β
ββββββββββββ¬βββββββ ββββββββββββββββββββ
β
ββββββββββββΌβββββββ
β PostgreSQL β
β (Prisma Postgresβ
β / Neon / etc.) β
βββββββββββββββββββ
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, Vite, Tailwind CSS v4 |
| Backend | Hono, TypeScript, Cloudflare Workers |
| Database | PostgreSQL via Prisma ORM |
| DB Connection Pooling | Prisma Accelerate (edge-compatible) |
| Caching / Rate Limiting | Upstash Redis |
| Edge KV Store | Cloudflare KV |
| AI Moderation | Groq SDK |
| Resend | |
| Cover Images | Unsplash API |
| Validation | Zod |
| Auth | JWT (Hono/jwt) + httpOnly cookie refresh tokens |
ink-and-fold/
βββ backend/ # Hono API β Cloudflare Worker
β βββ src/
β β βββ index.ts # All routes and middleware
β β βββ utils/
β β βββ auth.ts # OTP generation & hashing
β β βββ validator.ts # Zod schemas
β β βββ moderator.ts # Groq content moderation
β β βββ mailTemplate.ts# Resend email templates
β βββ prisma/
β β βββ schema.prisma # Database schema
β β βββ migrations/ # SQL migration files
β βββ wrangler.jsonc # Cloudflare Worker config (safe to commit)
β βββ prisma.config.ts # Prisma config for local migrations
β βββ .dev.vars # β οΈ Local secrets β gitignored, never commit
β βββ package.json
β
βββ frontend/ # React + Vite SPA
βββ src/
β βββ App.tsx # Router and route guards
β βββ api.ts # Axios instance + interceptors
β βββ types.ts # Shared TypeScript interfaces
β βββ components/ # All page/UI components
β βββ Landing.tsx
β βββ Signup.tsx / Signin.tsx / VerifyOtp.tsx
β βββ Blogs.tsx # Feed
β βββ BlogDetail.tsx # Single post view
β βββ Write.tsx # Markdown editor
β βββ Edit.tsx
β βββ MyPosts.tsx
β βββ Admin.tsx # Admin dashboard
β βββ UnsplashPicker.tsx
βββ package.json
- Node.js β₯ 18
- pnpm (recommended) or npm
- A Cloudflare Account (free tier works)
- A PostgreSQL database (e.g. Prisma Postgres, Neon, Supabase)
- A Prisma Data Platform account (for the Accelerate connection URL)
1. Install dependencies
cd backend
pnpm install2. Configure local secrets
Create a .dev.vars file in the backend/ directory:
DATABASE_URL="prisma+postgres://accelerate.prisma-data.net/?api_key=YOUR_PRISMA_ACCELERATE_KEY"
JWT_SECRET="your-strong-jwt-secret"
REFRESH_JWT_SECRET="your-strong-refresh-jwt-secret"
UNSPLASH_ACCESS_KEY="your-unsplash-access-key"
RESEND_API_KEY="your-resend-api-key"
GROQ_API_KEY="your-groq-api-key"
UPSTASH_REDIS_REST_URL="https://your-upstash-url.upstash.io"
UPSTASH_REDIS_REST_TOKEN="your-upstash-token"Note on DATABASE_URL: Two URLs are involved:
- Direct URL (e.g.
postgres://...) β used only for runningprisma migrate. Set this inbackend/.env.- Accelerate URL (e.g.
prisma+postgres://accelerate.prisma-data.net/...) β used by the Worker at runtime. Set this inbackend/.dev.vars.
3. Apply database migrations
Set your direct database URL in backend/.env, then run:
npx prisma migrate deploy4. Start the development server
pnpm devThe backend runs at http://localhost:8787.
1. Install dependencies
cd frontend
pnpm install2. Configure the API URL
Create a .env.local file in the frontend/ directory:
VITE_API_URL=http://localhost:87873. Start the dev server
pnpm devThe frontend runs at http://localhost:5173.
1. Authenticate with Cloudflare
npx wrangler login2. Deploy with secrets uploaded in one command
From the backend directory:
npx wrangler deploy --minify --secrets-file .dev.varsThis deploys the Worker and uploads all secrets from .dev.vars to Cloudflare in one step. Your live URL will be printed on success (e.g. https://backend.<your-subdomain>.workers.dev).
1. Set the VITE_API_URL environment variable to your deployed Worker URL in your hosting provider's dashboard.
2. Build the production bundle:
cd frontend
pnpm build3. Deploy the frontend/dist/ folder to your hosting provider.
All endpoints are prefixed with /api/v1. Protected routes require an Authorization: Bearer <token> header.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/signup |
β | Register a new user |
POST |
/verify-otp |
β | Verify email with OTP |
POST |
/signin |
β | Sign in, returns access + refresh token |
POST |
/refresh |
β | Refresh access token via cookie |
POST |
/signout |
β | Sign out current session |
POST |
/signout-all |
β | Revoke all sessions |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/blog |
β | Create a new post (triggers AI moderation) |
GET |
/blog/all |
β | Get all published posts |
GET |
/blog/user |
β | Get current user's posts |
GET |
/blog/bookmarks |
β | Get bookmarked posts |
GET |
/blog/:id |
β | Get a single post by ID |
PUT |
/blog/:id |
β | Update a post |
DELETE |
/blog/:id |
β | Delete a post |
POST |
/blog/:id/like |
β | Toggle like on a post |
POST |
/blog/:id/bookmark |
β | Toggle bookmark on a post |
POST |
/blog/:id/comment |
β | Add a comment |
GET |
/blog/:id/comments |
β | Get all comments for a post |
DELETE |
/blog/comment/:commentId |
β | Delete a comment |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
GET |
/unsplash/search |
β | Search Unsplash for cover images |
| Method | Endpoint | Description |
|---|---|---|
GET |
/admin/review-queue |
List posts pending review |
POST |
/admin/blog/:id/approve |
Approve a post for publishing |
POST |
/admin/blog/:id/reject |
Reject a post with a reason |
GET |
/admin/userslist |
Get all registered users |
POST |
/admin/promote/:userId |
Promote a user to admin |
POST |
/admin/users/:userId/ban |
Ban a user |
POST |
/admin/users/:userId/unban |
Unban a user |
| Variable | Used By | Description |
|---|---|---|
DATABASE_URL |
Worker (runtime) | Prisma Accelerate connection URL |
JWT_SECRET |
Worker | Secret for signing access tokens |
REFRESH_JWT_SECRET |
Worker | Secret for signing refresh tokens |
UNSPLASH_ACCESS_KEY |
Worker | Unsplash API key for image search |
RESEND_API_KEY |
Worker | Resend API key for transactional emails |
GROQ_API_KEY |
Worker | Groq API key for AI content moderation |
UPSTASH_REDIS_REST_URL |
Worker | Upstash Redis REST endpoint |
UPSTASH_REDIS_REST_TOKEN |
Worker | Upstash Redis auth token |
VITE_API_URL |
Frontend (build) | Backend base URL for the React app |
β οΈ Never commit.dev.varsor.envto version control. Both are already listed in.gitignore. Thewrangler.jsonc(including KV namespace IDs) is safe to commit β those IDs are resource identifiers, not secrets.
Contributions, issues and feature requests are welcome!
- Fork the repository
- Create a new branch:
git checkout -b feat/your-feature - Commit your changes:
git commit -m "feat: add your feature" - Push to your branch:
git push origin feat/your-feature - Open a Pull Request
For new database models, update prisma/schema.prisma and create a migration:
npx prisma migrate dev --name your_migration_nameMIT Β© Rituraj