A course-base fork, not a from-scratch build — the instructor's Bun/Fastify/Postgres scaffold got auth and a bare post model; I added likes, comments, search, pagination, rate limiting, a full custom frontend, and the Docker setup.
Note
Course project, clearly attributed. This started as ahmadardal/socialapp — the scaffold for a System-Level Quality Assurance course at Chas Academy. Auth, follow/unfollow, and the base post model are the instructor's. Everything in Status marked "added" is mine.
A photo-feed social app: register, follow people, post an image with a caption, and see a feed of posts from yourself and who you follow. On top of that base I built the interaction layer that makes it feel like a real app — likes, threaded comments, user search, paginated feeds, API-wide rate limiting — plus a custom React frontend and a Docker Compose setup so the whole thing (API, Postgres, frontend) runs with one command.
The sequence below is a real request path — register/login, post an image (uploaded to S3), like it, comment on it, then read it back through the paginated feed. Everything left of "Rate limiter" is base scaffold; the like, comment, and pagination steps are the features I added.
sequenceDiagram
actor User
participant API as Fastify API
participant RL as Rate limiter
participant S3 as AWS S3
participant DB as PostgreSQL
User->>API: POST /login
API->>DB: verify password (Bun.password.verify)
API-->>User: JWT (7d)
User->>API: POST /create (multipart: image + caption)
API->>RL: global check — 100 req/min
API->>S3: upload image
S3-->>API: image URL
API->>DB: INSERT post (status='active')
API-->>User: 201 created post
User->>API: POST /posts/:id/like
API->>DB: toggle row in reactions (insert or delete)
API-->>User: { liked: true|false }
User->>API: POST /posts/:id/comments
API->>DB: INSERT comment (parent_comment_id optional → threaded replies)
API-->>User: 201 created comment
User->>API: GET /feed?page=2
API->>DB: posts by self + followed users, ORDER BY created_at LIMIT 25 OFFSET
API-->>User: paginated feed
| Feature | Built by | State |
|---|---|---|
Register / login (JWT, Bun.password hashing) |
base scaffold | ✅ |
| Follow / unfollow, feed of followed users | base scaffold | ✅ |
| Image upload to S3, create post | base scaffold + hardening | ✅ |
Likes — toggle endpoint, reactions table, like counts |
me | ✅ |
Comments — threaded via parent_comment_id |
me | ✅ |
| Delete post, edit profile (incl. avatar upload) | me | ✅ |
User search (GET /users?search=) |
me | ✅ |
Feed pagination (?page=, LIMIT/OFFSET) |
me | ✅ |
Rate limiting — global 100/min + tighter limits on /register, /login |
me | ✅ |
| Custom React frontend — auth + feed pages, own components | me | ✅ |
| Docker setup — API, frontend, and Postgres containers via Compose | me | ✅ |
| Security/bug fixes from a code review pass | me | ✅ |
Requires Bun and Docker.
git clone https://github.com/devinder-dev/socialapp.git
cd socialapp
# create a .env with JWT_SECRET_KEY and S3_REGION / S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEY
docker compose up --build
# api → http://localhost:3000
# frontend → http://localhost:3002
# postgres → localhost:5433 (seeded from init.sql)Or run the API without Docker:
bun install
bun run dev # bun --hot --sql-preconnect ./src/index.tsFull feature list (click)
POST /register,POST /login— JWT auth, rate-limited to 10 requests / 10 minutesPOST /toggle-follow/:username,GET /users?search=— follow/unfollow and searchable user listPATCH /profile— edit display name, bio, avatar (multipart upload to S3)POST /create,GET /feed?page=,DELETE /posts/:id— create, paginated read, soft-delete (status flips todeleted, never hard-deleted)POST /posts/:id/like,GET /posts/:id/likes— toggle stored as an insert/delete on a(user_id, post_id)primary key, so a like can never double-countPOST /posts/:id/comments,GET /posts/:id/comments— comments support an optionalparent_comment_id, so a comment can reply to another comment- Global rate limiting (
@fastify/rate-limit, 100 req/min) plus per-route limits on auth endpoints - Media uploads go through an S3 adapter (
src/adapters/s3.ts) shared by post creation and profile avatars
- Custom React 19 SPA (
frontend/src/App.tsx) — no leftover scaffold UI, split intoAuthPageandFeedPage - Token stored client-side, feed fetched and re-fetched against the real API, create-post form posts
multipart/form-datadirectly
Dockerfile(API) andDockerfile.frontend, bothoven/bun:1-alpine, frozen-lockfile installsdocker-compose.yml— Postgres 16 (health-checked), API, and frontend as three services,init.sqlseeding the schema on first boot- Required secrets (
JWT_SECRET_KEY, S3 credentials) are pulled from the environment, not hardcoded
Architecture & repository layout (click)
flowchart TD
FE["React SPA<br/>(AuthPage · FeedPage)"] -->|fetch, Bearer JWT| API
subgraph API["Fastify API"]
direction TB
RL["Rate limiter<br/><i>global + per-route</i>"] --> R["Routes"]
R --> MW["authenticate (JWT)"]
MW --> C["Controllers<br/><i>parse request, shape response</i>"]
C --> Repo["Repositories<br/><i>raw SQL via bun:sql</i>"]
end
Repo --> PG[("PostgreSQL<br/>users · posts · reactions · comments")]
C --> S3[("AWS S3<br/>post images · avatars")]
src/
├── auth/ JWT plugin + authenticate preHandler
├── controllers/ HTTP-facing: parse request, call repository, shape response
├── repository/ raw SQL queries (bun:sql), one module per entity
├── routes/ route → controller wiring
├── schemas/ request validation (register, login)
├── adapters/ S3 upload adapter
├── db/ db client + schema
└── types/ shared DB / HTTP / auth types
frontend/
└── src/
├── App.tsx auth-state switch → AuthPage or FeedPage
└── pages/ AuthPage.tsx, FeedPage.tsx
Controllers stay thin — they pull data off the request, call a repository function, and return a status code. There's no separate service layer here (unlike a larger app); at this scale the controller/repository split was enough to keep SQL out of the HTTP layer.
Tech stack (click)
| Layer | Choice |
|---|---|
| Runtime | Bun 1.3 |
| API | Fastify 5, @fastify/jwt, @fastify/rate-limit, @fastify/multipart, @fastify/cors |
| Language | TypeScript |
| Database | PostgreSQL 16, queried directly via Bun's built-in sql client (no ORM) |
| Media storage | AWS S3 (@aws-sdk/client-s3) |
| Frontend | React 19, Bun's own dev server + bundler |
| DevOps | Docker, Docker Compose |
Devinder Singh · Fullstack Developer (YH) student at Chas Academy, Stockholm
Base scaffold by Ahmad Ardal. Everything under Status & what's mine marked "me" was built in this fork.