nelsonlai.dev: a personal site built like a product, with Postgres and Redis behind the blog
My personal website built with TypeScript, Next.js, and TailwindCSS
At a glance
- What is it?
- nelsonlai.dev is a personal blog and portfolio built with Next.js 16, Drizzle, oRPC and next-intl, and it pulls in Postgres, Redis, an authentication service and transactional email to run. The parts worth reading are the environment handling and the quality gates, not the blog posts.
- Who is it for?
- nelsonlai.dev fits a developer who wants a personal site with comments, likes, a guestbook, an admin dashboard and six languages, and who is prepared to run Postgres, Redis and an email preview server locally. It does not fit someone who wants a static site with no server state, because the comment, guestbook and account features all assume a database and an auth service.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 7 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A blog and portfolio that needs a database to render
nelsonlai.dev is described as a personal blog and portfolio website, and the repository description calls it a modern Next.js application for sharing thoughts, projects and insights. What makes it more than a blog template is the amount of application machinery behind it.
The feature list includes a guestbook, an admin dashboard with live stats, and account settings, so the site has users and state, not just pages. The blog side adds a comment system, a like button, a post view counter, search across posts, an RSS feed and a sitemap. Authentication is handled by Better Auth, images go to Cloudflare R2, transactional email goes out through Resend, caching and rate limiting use Upstash Redis, and analytics use Umami.
The core stack is Next.js 16 with the App Router and React Compiler, React 19, TypeScript with strict configuration, Tailwind CSS v4, MDX for content through Content Collections, Drizzle ORM with Zod schemas, next-intl for six locales, and oRPC as the type-safe API layer. The interface uses Base UI for accessible components, Shiki for code highlighting and Motion for animation, with a command menu driven from the keyboard, a table of contents, image zoom in posts and light and dark modes.
None of that is wrong for a personal site. It is simply a much larger system than a static site, and every part of it becomes a dependency you inherit.
Varlock owns the environment and generates the types
The environment handling is the most transferable idea in the repository, and it starts before the database. The README says to create .env.local using the variables documented in .env.schema, and the local Docker services start with a specific set of values:
DATABASE_URL=postgres://postgres:postgres@localhost:5432/nelsonlaidev
UPSTASH_REDIS_REST_URL=http://localhost:8079
UPSTASH_REDIS_REST_TOKEN=local-uprest-token-7f3c9a2e6b1d4f80
IP_ADDRESS_SALT=
BETTER_AUTH_SECRET=The two blank values are the ones you must fill in. The README asks for separate random values of at least 32 characters for IP_ADDRESS_SALT and BETTER_AUTH_SECRET, for example with openssl rand -hex 32, and makes one operational point that is easy to miss: keep an existing IP_ADDRESS_SALT stable to preserve anonymous identifiers. Change it and you break the link between a returning visitor and their previous anonymous identity.
Varlock then validates the configuration against .env.schema, loads .env.local automatically and generates env.d.ts. Next.js uses the official Varlock integration, database scripts and test runners and Knip load configuration through varlock/auto-load in their Node.js entry points, and external CLIs such as React Email are run through varlock run. Application code reads typed values directly from varlock/env rather than reaching for process.env.
The result is a schema that is the single source of truth, a generated type file, and a validation step you can run on its own:
pnpm env:checkThree containers stand in for the hosted services
The local environment runs on Docker Compose, and the compose file is a good picture of what the application expects from its infrastructure. A Postgres 16 on Alpine serves the Drizzle schema, a plain Redis serves the cache, and a third service called uprest provides the serverless Redis REST endpoint that the Upstash client talks to.
That third service is the interesting one. The image is ghcr.io/nelsonlaidev/uprest:v0.4.0, it listens on port 8079 mapped from 8080, it takes the same REST token the application uses, and its connection string points at the Redis container:
uprest:
image: ghcr.io/nelsonlaidev/uprest:v0.4.0
container_name: nelsonlaidev-uprest
ports:
- 8079:8080
environment:
UPREST_TOKEN: local-uprest-token-7f3c9a2e6b1d4f80
UPREST_CONNECTION_STRING: redis://redis:6379The Redis service has a healthcheck built on redis-cli ping with a five second interval and five retries, and the uprest service declares a dependency on Redis with condition: service_healthy, so the proxy only starts once the cache is actually answering. The three services share a bridge network, and Postgres and Redis both have named volumes for their data.
The endpoints that result are listed in the README, and the split is informative: the app on localhost:3000, the React Email preview on 3001, Postgres on 5432, Redis on 6379, and the Redis serverless endpoint on 8079.
Migrations, seeds, and two development servers
Once the containers are up, the sequence is short and explicit. Migrate the database, then seed it, then run the app:
pnpm db:migratepnpm db:seedpnpm dev # Run the development server
# or
pnpm email:dev # Run the email preview server separatelyThe email preview is a separate process on its own port because the site sends comment and reply notification emails, and the README includes screenshots of both templates. The email script is wrapped in varlock run so the external CLI sees the same validated configuration the app does.
The script list in the README and the package.json tell you what the project treats as routine. There is a build, a start, a bundle analysis with next experimental-analyze, an MDX collection build with content-collections build, and a typegen step that runs the MDX build and then next typegen. The database has its own family of scripts: check, generate, migrate, push, reset and studio, all driven by drizzle-kit, with reset and seed written as TypeScript files under src/db/.
That is a full developer workflow rather than a demo, and it is why the project also carries drizzle.config.ts, content-collections.ts, global.d.ts and a set of per-tool configuration files at the root.
Six locales, and a check that the translations exist
Internationalisation is handled by next-intl across six locales, and the project treats missing translations as a build failure rather than a runtime surprise. The check:i18n script is the clearest statement of that:
pnpm check:i18n # Validate translationsThe underlying command is i18n-check with the source language en, the messages directory at ./src/i18n/messages, the usage directory at ./src and the next-intl formatter, and the README's description of it is a translation validation step. The i18n-check tool appears in the development experience list alongside the other quality tooling, and the src/i18n/ directory in the project structure is where the messages live.
The site structure reflects a full app router layout. The src/ directory is broken into app/ for the router pages, components/, contexts/, hooks/, constants/ and lib/ for utilities, content/ for the MDX blog posts, db/ for the schema and migrations, emails/ for the templates, orpc/ for the API routes, mdx-plugins/ for custom rehype and remark plugins, assets/, styles/, and tests/ for unit and end to end tests.
The presence of custom remark and rehype plugins is worth noting. It means the MDX pipeline is modified at the compiler level rather than only at the component level, which is how a site can do things like image zoom or a table of contents without bolting them onto every page by hand.
The quality gates are wired into one command
The check script is the piece to copy if you are building something similar. It chains lint, typecheck, format check, knip, the i18n check and the spell check into a single command:
pnpm check # Run all quality checks (lint, typecheck, format, etc.)Each of those is a specific tool with a specific job. Oxlint does the linting, with lint:fix for automatic fixes. Oxfmt does the formatting, with format:check to verify it. Knip finds unused dependencies and exports, which matters in a project with this many packages. CSpell does the spell checking through lint:spell, and it has a .cspell.json and a .cspell/ directory at the root. TypeScript checking is tsc --noEmit.
Tests are split by kind. Vitest runs the unit tests with coverage and watch and UI variants, and Playwright runs the end to end tests with debug, UI and install scripts.
The commit-time side is handled by Lefthook, with lefthook.yml at the root and a prepare script that installs the hooks, plus conventional commits as the message format. CI is GitHub Actions. The README also claims a Lighthouse score of nearly 100 and describes the SEO work as meta tags and JSON-LD with dynamic open graph images rendered through next/og.
The licence is MIT with the LICENSE file at the root, the last push was on 2026-10-19, and the repository is not archived.
Where this repository is the wrong starting point
The first wrong fit is a static site. If you want a blog with no server state, this architecture is the wrong shape for it. Comments, likes, view counts, a guestbook, accounts and an admin dashboard all assume a database, and the Redis cache and rate limiter assume a running service. A static site generator would give you the writing experience with none of that, and the trade is simply whether you want interaction.
The second is a machine without Docker. The prerequisites are Node.js 24 or newer, pnpm 12.7 or newer and Docker, and the local database and cache both come from containers. Varlock also generates env.d.ts, so the environment tooling assumes a working pnpm and Node setup before it can validate anything.
The third is a team that wants to own the data layer. Drizzle with Zod schemas, drizzle-kit for migrations and resets, and a custom uprest image for the Redis REST endpoint are all reasonable, but they are choices rather than defaults, and the uprest image in particular is a project-specific dependency pinned to a version. Swapping in a hosted Redis means changing the compose file and the environment variables together.
The fourth is treating it as a template. The repository is a personal site with a guestbook and an admin dashboard, which means there is no separation between the reusable parts and the personal parts. The parts worth taking are the Varlock setup, the single check command, the i18n validation and the Drizzle workflow. The guestbook, the likes and the Umami analytics are decisions about one particular site, and copying them means copying someone's site rather than someone's tooling.
Editorial conclusion
nelsonlai.dev fits a developer who wants a personal site with comments, likes, a guestbook, an admin dashboard and six languages, and who is prepared to run Postgres, Redis and an email preview server locally. It does not fit someone who wants a static site with no server state, because the comment, guestbook and account features all assume a database and an auth service. Before adopting the stack, set the two secrets the README asks for and run pnpm env:check, since the environment is validated before anything else runs.
Frequently asked questions
How do I run nelsonlai.dev locally?
Install Node.js 24 or newer, pnpm 12.7 or newer and Docker. Clone the repository, run pnpm install, create .env.local from .env.schema, validate with pnpm env:check, then bring up the services with docker compose up -d, run pnpm db:migrate and pnpm db:seed, and start the app with pnpm dev.
What does Varlock do in this project?
Varlock validates the environment against .env.schema, loads .env.local automatically and generates env.d.ts. Next.js uses the official Varlock integration, and application code reads typed values from varlock/env rather than from process.env.
Which local services does docker compose start?
Postgres 16 on Alpine, a plain Redis, and an uprest container that exposes a Redis REST endpoint on port 8079 for the Upstash client. The app runs on port 3000 and the React Email preview on 3001.
What does the pnpm check command run?
It runs lint, typecheck, format:check, knip, check:i18n and lint:spell in sequence. Linting is Oxlint, formatting is Oxfmt, unused code detection is Knip, spell checking is CSpell and translation validation is i18n-check.
What licence is nelsonlai.dev under?
The project is MIT licensed, with the LICENSE file at the root of the repository, and the package.json also declares the MIT license with version 0.0.0.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nelsonlaidev-nelsonlai-dev)Was this useful?