Byte of Me is a full-stack system where I can manage content (blogs, projects, education) from a private dashboard and render everything on a fast public site.
This post explains how I built it — focusing on architecture, data flow, and key decisions.
System Overview
At a high level, the system has 3 main parts:
Frontend (Next.js App Router)
Backend logic (server actions + API)
Database & storage (Supabase + Prisma)
Monorepo Architecture
Instead of putting everything in one folder, I used a monorepo:
apps/
web/ → Next.js app (UI + API)
packages/
db/ → Prisma schema + client
storage/ → Upload logic (Supabase Storage)
logger/ → Shared loggingWhy?
Reuse logic across apps
Keep separation of concerns
Scale easier in the future
Frontend: Next.js App Router
I use Next.js App Router with:
Server Components → for data fetching
Client Components → for interactivity
Server Actions → for mutations (create/update/delete)
Example flow
Page loads → Server Component fetches data
User edits → Client Component triggers Server Action
Data updates → UI revalidates (React Query or revalidateTag)
Backend: Prisma + Supabase
Query example
const blog = await prisma.blog.findUnique({
where: { id },
include: { translations: true },
});Why this stack?
Supabase → easy hosting + storage
Prisma → type-safe queries
Data Layer: Prisma + Supabase
Database
PostgreSQL hosted on Supabase
Prisma used as ORM
Example:
const blog = await prisma.blog.findUnique({
where: { slug },
include: { translations: true, tags: true },
});
Storage: Supabase (S3-like)
Images are stored in Supabase Storage.
Flow:
Upload file → Supabase bucket
Save
mediain the database.
Authentication: Auth.js
Auth is handled by Auth.js:
JWT-based authentication
Protected routes (dashboard)
Example
const user = await requireUser(); // server-side guard, e.g. dashboardIf no session → redirect to login.
State Management: TanStack Query
I use TanStack Query for:
Fetching data on client
Caching
Mutations
Internationalization (i18n)
I use 2 types of translation:
1. Static (UI) — next-intl
For UI text like buttons, labels:
{
"showMore": "Show more",
"showLess": "Show less"
}const t = useTranslations();
t('showMore');2. Dynamic (Content)
For blogs/projects stored in database:
translations: [
{ language: 'en', title: '', content: '' },
{ language: 'vi', title: '', content: '' }
];Select by current language:
const t = blog.translations.find(
(x) => x.language === locale
);