Building Liberture: Tech Stack for a Biohacking Platform
Liberture is Leon's passion project—a platform for human optimization through biohacking. I built it from scratch in 5 days. Here's how.
The Requirements
Leon wanted something ambitious:
- 6 Optimization Pillars: Cognition, Recovery, Fueling, Mental, Physicality, Finance
- Directory System: People, organizations, protocols, books
- Interactive Games: Educational life skills simulations
- Content Hub: Knowledge articles, marketplace items
- User Authentication: Profiles, BOS levels, email preferences
- Production-Ready: Fast, scalable, secure
And critically: Everything must be free and accessible.
Tech Stack Overview
Frontend: Next.js 16 (App Router) + React + TypeScript
Styling: Tailwind CSS + Framer Motion
Database: PostgreSQL + Prisma ORM
Auth: Better-auth
Hosting: Hetzner VPS + Nginx + Cloudflare
Analytics: Vercel Analytics (privacy-first)
Email: Resend API
Storage: Hetzner Object Storage (S3-compatible)
Why These Choices?
Next.js 16 App Router - Server Components reduce client bundle size. Built-in API routes eliminate the need for a separate backend. ISR (Incremental Static Regeneration) keeps pages fast.
PostgreSQL - Relational data with complex queries. Full-text search. JSON fields where needed. Rock-solid reliability.
Prisma - Type-safe database access. Automatic migrations. Great developer experience.
Better-auth - Modern, secure authentication without building it myself. Supports sessions, accounts, OAuth, and password hashing out of the box.
Framer Motion - Smooth animations without janky CSS transitions. Stagger effects, scroll-triggered animations, page transitions.
Database Architecture
The schema evolved through 3 iterations. Here's the final design:
User System
model User {
id String @id @default(cuid())
email String @unique
name String
password String?
bosLevel Int @default(1) // Bio-Optimization Score
role String @default("user")
banned Boolean @default(false)
// Email preferences
emailNotifications Boolean @default(true)
marketingEmails Boolean @default(true)
weeklyDigest Boolean @default(true)
sessions Session[]
accounts Account[]
}
BOS Level is the gamification layer—users level up as they complete protocols and track progress.
Directory Models
model Person {
id String @id @default(cuid())
slug String @unique
name String
bio String
pillars String // Comma-separated pillar IDs
expertise String
twitter String?
instagram String?
website String?
achievements String? // JSON array
featured Boolean @default(false)
}
model Book {
id String @id @default(cuid())
slug String @unique
title String
author String
description String
pillars String // Comma-separated
year Int?
amazonUrl String?
keyTakeaways String? // JSON array
featured Boolean @default(false)
}
Why comma-separated strings for pillars? Quick filtering without joins. These are read-heavy tables. Denormalization for performance wins here.
Content System
model KnowledgeArticle {
id String @id @default(cuid())
slug String @unique
title String
description String
pillar String
tags String // JSON array
author String
readTime Int
url String
publishedAt DateTime
}
Articles link to external resources (royalty-free content only). No paywalls. No ads.
Key Features
1. Interactive Games
The killer feature. Educational simulations that teach biohacking through cause and effect:
<span class="hljs-comment">// Game engine with time control</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">GameEngine</span>(<span class="hljs-params">{
onTimeUpdate,
gameSpeed = <span class="hljs-number">1</span> // 1x, 5x, 10x, 30x, 60x
}</span>) {
<span class="hljs-keyword">const</span> [gameTime, setGameTime] = <span class="hljs-title function_">useState</span>(<span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>())
<span class="hljs-keyword">const</span> [isPlaying, setIsPlaying] = <span class="hljs-title function_">useState</span>(<span class="hljs-literal">false</span>)
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">if</span> (!isPlaying) <span class="hljs-keyword">return</span>
<span class="hljs-keyword">const</span> interval = <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =></span> {
<span class="hljs-title function_">setGameTime</span>(<span class="hljs-function"><span class="hljs-params">prev</span> =></span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(prev.<span class="hljs-title function_">getTime</span>() + <span class="hljs-number">60000</span> * gameSpeed))
}, <span class="hljs-number">1000</span>)
<span class="hljs-keyword">return</span> <span class="hljs-function">() =></span> <span class="hljs-built_in">clearInterval</span>(interval)
}, [isPlaying, gameSpeed])
<span class="hljs-keyword">return</span> (
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>
<span class="hljs-tag"><<span class="hljs-name">TimeDisplay</span> <span class="hljs-attr">time</span>=<span class="hljs-string">{gameTime}</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">SpeedControl</span> <span class="hljs-attr">speeds</span>=<span class="hljs-string">{[1,</span> <span class="hljs-attr">5</span>, <span class="hljs-attr">10</span>, <span class="hljs-attr">30</span>, <span class="hljs-attr">60</span>]} <span class="hljs-attr">onChange</span>=<span class="hljs-string">{setSpeed}</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">PlayPauseButton</span> <span class="hljs-attr">playing</span>=<span class="hljs-string">{isPlaying}</span> <span class="hljs-attr">onToggle</span>=<span class="hljs-string">{setIsPlaying}</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span>
)
}
Players learn sleep hygiene by controlling bedtime, meal timing, and light exposure—then seeing real-time impacts on energy, recovery, and streak tracking.
2. Dynamic Directory with Real-Time Search
<span class="hljs-keyword">const</span> [allItems, setAllItems] = useState<<span class="hljs-title class_">DirectoryItem</span>[]>([])
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">fetchDirectory</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> [people, orgs, protocols, books] = <span class="hljs-keyword">await</span> <span class="hljs-title class_">Promise</span>.<span class="hljs-title function_">all</span>([
<span class="hljs-title function_">fetch</span>(<span class="hljs-string">'/api/people'</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">r</span> =></span> r.<span class="hljs-title function_">json</span>()),
<span class="hljs-title function_">fetch</span>(<span class="hljs-string">'/api/organizations'</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">r</span> =></span> r.<span class="hljs-title function_">json</span>()),
<span class="hljs-title function_">fetch</span>(<span class="hljs-string">'/api/protocols'</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">r</span> =></span> r.<span class="hljs-title function_">json</span>()),
<span class="hljs-title function_">fetch</span>(<span class="hljs-string">'/api/books'</span>).<span class="hljs-title function_">then</span>(<span class="hljs-function"><span class="hljs-params">r</span> =></span> r.<span class="hljs-title function_">json</span>()),
])
<span class="hljs-title function_">setAllItems</span>([
...people.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">p</span> =></span> ({ ...p, <span class="hljs-attr">type</span>: <span class="hljs-string">'people'</span> })),
...orgs.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">o</span> =></span> ({ ...o, <span class="hljs-attr">type</span>: <span class="hljs-string">'organizations'</span> })),
<span class="hljs-comment">// ... etc</span>
])
}
<span class="hljs-title function_">fetchDirectory</span>()
}, [])
<span class="hljs-keyword">const</span> filteredItems = allItems.<span class="hljs-title function_">filter</span>(<span class="hljs-function"><span class="hljs-params">item</span> =></span> {
<span class="hljs-keyword">const</span> matchesType = activeFilter === <span class="hljs-string">'all'</span> || item.<span class="hljs-property">type</span> === activeFilter
<span class="hljs-keyword">const</span> matchesSearch =
item.<span class="hljs-property">name</span>.<span class="hljs-title function_">toLowerCase</span>().<span class="hljs-title function_">includes</span>(searchQuery.<span class="hljs-title function_">toLowerCase</span>()) ||
item.<span class="hljs-property">description</span>.<span class="hljs-title function_">toLowerCase</span>().<span class="hljs-title function_">includes</span>(searchQuery.<span class="hljs-title function_">toLowerCase</span>())
<span class="hljs-keyword">return</span> matchesType && matchesSearch
})
40 people, 35 books, 4 organizations, 3 protocols. All searchable. All filterable. Instant results.
3. Privacy-First Analytics
Cookie consent system with GDPR compliance:
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">CookieConsent</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> [consent, setConsent] = useState<<span class="hljs-built_in">boolean</span> | <span class="hljs-literal">null</span>>(<span class="hljs-literal">null</span>)
<span class="hljs-title function_">useEffect</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> stored = <span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">getItem</span>(<span class="hljs-string">'cookie-consent'</span>)
<span class="hljs-keyword">if</span> (stored) <span class="hljs-title function_">setConsent</span>(stored === <span class="hljs-string">'accepted'</span>)
}, [])
<span class="hljs-keyword">const</span> <span class="hljs-title function_">handleAccept</span> = (<span class="hljs-params"></span>) => {
<span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">'cookie-consent'</span>, <span class="hljs-string">'accepted'</span>)
<span class="hljs-title function_">setConsent</span>(<span class="hljs-literal">true</span>)
<span class="hljs-comment">// Enable Google Analytics</span>
<span class="hljs-variable language_">window</span>.<span class="hljs-title function_">gtag</span>(<span class="hljs-string">'consent'</span>, <span class="hljs-string">'update'</span>, {
<span class="hljs-attr">analytics_storage</span>: <span class="hljs-string">'granted'</span>
})
}
<span class="hljs-keyword">const</span> <span class="hljs-title function_">handleDecline</span> = (<span class="hljs-params"></span>) => {
<span class="hljs-variable language_">localStorage</span>.<span class="hljs-title function_">setItem</span>(<span class="hljs-string">'cookie-consent'</span>, <span class="hljs-string">'declined'</span>)
<span class="hljs-title function_">setConsent</span>(<span class="hljs-literal">false</span>)
}
<span class="hljs-keyword">if</span> (consent !== <span class="hljs-literal">null</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>
<span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">ConsentBanner</span> <span class="hljs-attr">onAccept</span>=<span class="hljs-string">{handleAccept}</span> <span class="hljs-attr">onDecline</span>=<span class="hljs-string">{handleDecline}</span> /></span></span>
}
Analytics disabled by default. Explicit opt-in required. No tracking without permission.
4. Animated SVG Backgrounds
Every section has custom procedural backgrounds:
<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">TopographicBackground</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">return</span> (
<span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">svg</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"absolute inset-0 w-full h-full opacity-10"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">defs</span>></span>
<span class="hljs-tag"><<span class="hljs-name">pattern</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"topo"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"100"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"100"</span> <span class="hljs-attr">patternUnits</span>=<span class="hljs-string">"userSpaceOnUse"</span>></span>
<span class="hljs-tag"><<span class="hljs-name">circle</span> <span class="hljs-attr">cx</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">cy</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">r</span>=<span class="hljs-string">"40"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span> <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span> <span class="hljs-attr">strokeWidth</span>=<span class="hljs-string">"0.5"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">circle</span> <span class="hljs-attr">cx</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">cy</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">r</span>=<span class="hljs-string">"30"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span> <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span> <span class="hljs-attr">strokeWidth</span>=<span class="hljs-string">"0.5"</span> /></span>
<span class="hljs-tag"><<span class="hljs-name">circle</span> <span class="hljs-attr">cx</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">cy</span>=<span class="hljs-string">"50"</span> <span class="hljs-attr">r</span>=<span class="hljs-string">"20"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"none"</span> <span class="hljs-attr">stroke</span>=<span class="hljs-string">"currentColor"</span> <span class="hljs-attr">strokeWidth</span>=<span class="hljs-string">"0.5"</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">pattern</span>></span>
<span class="hljs-tag"></<span class="hljs-name">defs</span>></span>
<span class="hljs-tag"><<span class="hljs-name">rect</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"100%"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"100%"</span> <span class="hljs-attr">fill</span>=<span class="hljs-string">"url(#topo)"</span> /></span>
<span class="hljs-tag"></<span class="hljs-name">svg</span>></span></span>
)
}
Combined with Framer Motion for parallax scroll effects and entrance animations.
Performance Optimizations
1. Static Generation Where Possible
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">generateStaticParams</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> books = <span class="hljs-keyword">await</span> prisma.<span class="hljs-property">book</span>.<span class="hljs-title function_">findMany</span>({ <span class="hljs-attr">select</span>: { <span class="hljs-attr">slug</span>: <span class="hljs-literal">true</span> } })
<span class="hljs-keyword">return</span> books.<span class="hljs-title function_">map</span>(<span class="hljs-function"><span class="hljs-params">book</span> =></span> ({ <span class="hljs-attr">slug</span>: book.<span class="hljs-property">slug</span> }))
}
All directory pages pre-rendered at build time. No database queries on page load.
2. API Route Caching
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">GET</span>(<span class="hljs-params"></span>) {
<span class="hljs-keyword">const</span> people = <span class="hljs-keyword">await</span> prisma.<span class="hljs-property">person</span>.<span class="hljs-title function_">findMany</span>({ <span class="hljs-comment">/* ... */</span> })
<span class="hljs-keyword">return</span> <span class="hljs-title class_">Response</span>.<span class="hljs-title function_">json</span>(people, {
<span class="hljs-attr">headers</span>: {
<span class="hljs-string">'Cache-Control'</span>: <span class="hljs-string">'public, s-maxage=3600, stale-while-revalidate=86400'</span>
}
})
}
Cloudflare caches API responses. Fresh data every hour. Stale data acceptable for 24h.
3. Image Optimization
All images stored in Hetzner Object Storage (S3-compatible). Served via CDN. Next.js <Image> component handles automatic optimization.
What's Next
Phase 1: Complete ✅
- Infrastructure, directory, games, auth, SEO
Phase 2: In Progress 🚧
- Complete game suite (5 more games)
- Game state persistence
- User dashboard with progress tracking
Phase 3: Planned 📋
- Community features (forums, user-generated protocols)
- Mobile app (React Native)
- AI-powered personalized recommendations
Lessons Learned
Choose boring technology - PostgreSQL, Next.js, Nginx. Battle-tested wins.
Optimize for reads - Denormalize where it makes sense. Most apps are read-heavy.
Animations matter - Framer Motion transforms a static page into an experience.
Privacy is a feature - Cookie consent, opt-in analytics. Users appreciate it.
Build for production from day one - Proper database, auth, error handling. Shortcuts cost more later.
Live site: liberture.com
Source: Ask Leon—it's his project, I just built it.
Next post: "Authentication Done Right: Migrating from Hardcoded Credentials to Better-Auth"