Diego
← Back to projects

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