Linkiess
Link-in-bio platform — NestJS API + React front, with profile customization and S3 upload.
Back-end (API)
- NestJS 9
- TypeScript
- Prisma 4.5
- PostgreSQL
- Passport.js (Local + JWT)
- bcrypt
- Multer
- AWS S3 SDK
- class-validator
Front-end
- React 18
- React Router DOM 6
- Redux Toolkit
- Axios
- Tailwind CSS 3
- jwt-decode
- React Icons
Full-screen for best quality.
Context
Linkiess is a link-in-bio platform where users concentrate all their links on a personalized public page. After signing up, each user has a profile at /{username} and a private editing area. The profile is customizable: background image, profile photo, description, text color and bar colors. Images are stored on AWS S3.
The back-end (NestJS 9 + Prisma 4.5 + PostgreSQL) implements Passport.js auth (Local + JWT), two-stage image upload (disk → S3), global route protection with per-decorator opt-out, and token-based ownership verification.
The front-end (React 18 + Redux Toolkit + Tailwind) manages auth and global messages via Redux, verifies profile ownership by decoding the JWT on the client, and uses a boolean trigger in local state as a re-fetch mechanism after mutations.
Technical decisions
Front · Redux Toolkit for auth + global feedback
Auth state (isLogged, username) lives in Redux persisted to localStorage. The same store manages success/error messages via messageSlice — no external toast library.
Front · JWT decoded on the client before sensitive operations
Before rendering the customization screen, the token is decoded with jwt-decodeand the payload’s username is compared against the URL and localStorage. Any divergence logs out and redirects the user.
Front · Boolean trigger as post-mutation re-fetch mechanism
Write actions (image upload, edit link, change color) toggle a boolean trigger in local state. The page’s useEffect depends on this value — when it flips, it re-fetches the profile data.
API · Two-stage upload: disk → S3
Multer saves the file to ./uploads (local disk). After writing, S3Storage.saveFile() reads the buffer, puts the object in the bucket with ACL public-read and deletes the temp file via fs/promises unlink. Multer requires a synchronous destination at multipart parse time — the local stage acts as a staging area before persisting to object store.
API · Global guard + @IsPublic() opt-out
JwtAuthGuard is applied globally in the root module — all routes are protected by default. Public routes (account creation, profile read) receive the @IsPublic() decorator, which sets isPublic: true metadata. The guard reads this metadata before validating the token; if present, it passes.
API · Ownership verification via jwt_decode in the service
Edit routes receive the Authorization header and pass it to authorizationMiddleware() in the service, which uses jwt_decodeto extract the payload username and compares it with the resource’s id_user. On divergence, throws 401. The guard validates signature + expiration via Passport; the middleware validates that the token owner is the resource owner.
Run locally
Requires: Docker and Git installed.
mkdir linkiess-local && cd linkiess-local
git clone https://github.com/odgiedev/linkiess
git clone https://github.com/odgiedev/linkiess-api
cp linkiess/.env.example linkiess/.env
cp linkiess-api/.env.example linkiess-api/.env
# Terminal 1 — API
cd linkiess-api && docker compose up
# Terminal 2 — Front
cd linkiess && docker compose up