# Satus: a Next.js 16 starter whose first page teaches you how to use it

> Satus is a darkroom.engineering starter for content-driven sites built on Next.js 16, React 19, Tailwind v4 and Bun, with opt-in Sanity, Shopify, HubSpot and Mailchimp integrations that a setup script strips out when unused. Its distinctive choice is encrypted environment variables, with the private key never in git and CI deliberately building with every integration switched off.

**darkroomengineering/satus** — Advanced Next.js App Router starter for content-driven sites

- Repository: https://github.com/darkroomengineering/satus
- Website: https://satus.darkroom.engineering
- Stars: 1,013 · Forks: 97
- Language: TypeScript
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/darkroomengineering-satus

## The landing page is the manual

Satus is a Next.js 16 starter with React 19, Tailwind CSS v4 and optional WebGL, from darkroom.engineering. The name is Latin for beginning, and the way the project teaches is its most distinctive feature.

When you clone it and run the dev server, the landing page is not a marketing page. It is a step-by-step manual that walks you from a fresh clone to a shippable site, and the README is explicitly the reference version of that manual rather than the only copy.

That design choice answers the oldest problem with starters. A repository full of clever abstractions is useless if the person who cloned it cannot tell which parts to keep and which to delete. Here the first screen in the browser is the onboarding, and it is regenerated from the same conventions the code follows. The README also notes that this document is for template developers, while client handoff has its own `PROD-README.md`, which suggests the project expects to be handed to someone else after the build rather than only cloned by its author.

The stack underneath is current but specific. Next.js 16 with the App Router, static pages and tag-based revalidation, React Compiler, strict TypeScript, and Tailwind v4 alongside CSS Modules. The Tailwind and CSS Module combination is described as sitting side by side under one cascade contract, so utility styles and module styles cannot quietly fight each other, which is a real problem when a project mixes both.

The audience is a studio or an engineer who wants a modern, visually ambitious site skeleton without assembling one from scratch, and who is willing to delete most of it.

## Bun, oxlint and oxfmt replace the usual toolchain

The runtime is Bun, and the linting and formatting is done with the oxc tools rather than the ESLint and Prettier pairing most Next.js projects carry. The README states that `oxlint` and `oxfmt` cover TypeScript, CSS, Markdown, YAML and TOML, and that they sort imports and Tailwind classes at format time.

That last point is not a small convenience. Sorting Tailwind classes at format time removes the class order churn that produces noisy diffs in a Tailwind codebase, and covering Markdown, YAML and TOML means the same formatter handles the configuration files and the content as well as the source.

The requirements are pinned in the README, and the install is two commands:

```bash
bun install
bun dev       # open localhost:3000 for the manual
```

The README asks for Node.js 24.20 or newer, and Bun 1.4.0 or newer. Bun serves three roles in this project. It is the runtime for the dev and build scripts, it is the test runner, and it is the package manager, with `bun.lock` and `bunfig.toml` in the tree. The `bunfig.toml` file has a job beyond package management, since it turns off Bun's own `.env` loading, which would otherwise set still-encrypted values before Next.js gets a chance to decrypt them.

The scripts show the same philosophy. `dev` runs `bun ./lib/scripts/dev.ts`, and there is a `dev:https` variant and a `dev:inspect` one, so the dev server is a small script rather than a raw `next dev` call. The build runs `bun run setup:styles` before `next build`, which suggests the stylesheet pipeline is generated rather than committed. There are also `lighthouse` and `bench:nav` scripts for performance checks, an `analyze` script that sets an `ANALYZE` flag for the build, and `analyze:experimental` for `next experimental-analyze`.

The repository also carries `lefthook.yml` for git hooks, `playwright.config.ts` and an `e2e/` directory, so end-to-end testing is part of the template rather than something a consumer adds later.

## Integrations are plugins, and setup:project deletes the ones you do not use

The opt-in model is the core of the template. Sanity, Shopify, HubSpot and Mailchimp live isolated under `lib/integrations`, and WebGL lives under `lib/webgl` behind `lib/features`, which are feature flags gating opt-in surfaces. The `setup:project` script strips the rest.

The distinction is between code that is present and code that is active. The `.env.example` file states that nothing in it is required to boot the starter, and that every integration is optional and self-disables when its env vars are absent. So a fresh clone runs, and a site with none of the four services configured behaves like a site with no third-party services at all.

The trimming script is the interesting half. It removes unused integrations, and the README says it does so interactively, stripping code, dependencies and env vars, with the non-interactive flags documented in `lib/integrations/README.md`. Removing the dependencies as well as the code is what makes the difference real, since a template that keeps four unused SDKs in its lockfile still carries their weight in install time and in your supply chain.

The project structure is built around the same rule. The README gives the mental model directly: UI goes in `components/`, everything else goes in `lib/`. Inside `lib/` sit the hooks, the integrations, the feature flags, the WebGL code, the SEO helpers, the utilities, the CLI scripts, the styles and optional debug tools. The app directory holds the routes, and the routing comment is unusually specific: `(site)/page.tsx` is the manual, the root layout is a bare shell shared with `/studio`, and `llms.txt/`, `agent-content/`, `sitemap.ts`, `robots.ts` and `manifest.ts` sit at the app root as AEO surfaces.

That last group is worth a second look. Having `llms.txt` and an `agent-content` directory as first-class routes in a starter, next to the sitemap and robots files, tells you the template treats machine-readable surfaces as a first-class requirement rather than an afterthought.

## An encrypted .env where the private key never enters git

The environment variable story is the most opinionated part of the project, and it is worth reading closely before you clone it.

Satus itself ships no env file. In a fork, the first `bun dotenvx set` call creates the `.env` and its own key, and the reason given is specific: a shared `.env` would make every fork encrypt to satus's key. The `.env` file is committed, encrypted with dotenvx, and every value in it is encrypted, including public ones. Next loads the file in `bun dev`, in local builds and in Vercel production and preview, and the test file `lib/env-files.test.ts` fails on a plain, unencrypted one. Decryption is handled by `@dotenvx/next-env`, and `lib/env.ts` throws on any value it could not decrypt, so nothing runs half-configured.

The base URL is deliberately not in the file. On Vercel, production uses `VERCEL_PROJECT_PRODUCTION_URL` and previews use `VERCEL_BRANCH_URL`, so share cards and absolute links on a preview point at that preview, and locally it falls back to localhost. `NEXT_PUBLIC_BASE_URL` only overrides it, which matters when the shortest production custom domain is not the one you want canonicalised.

Overrides are per key and per environment. A key that needs a different value locally or in a build goes into an encrypted `.env.development` or `.env.production` with `bun dotenvx set KEY "value" -f.env.production`, and each file has its own key. `.env.local` is still loaded first and stays out of git, for personal overrides.

The key handling is the part with the most operational detail. Starting a project means running `bun dotenvx set` per variable, committing `.env`, then running `bun run env:setup`, which sends the key to the linked Vercel project for production and preview without printing it, and which needs the folder linked with `vercel link`. Joining a project means getting the `DOTENV_PRIVATE_KEY="..."` line and putting it in `.env.keys` at the repo root, and `.env.keys` is git-ignored. Vercel only needs the private keys, `DOTENV_PRIVATE_KEY` plus `DOTENV_PRIVATE_KEY_PRODUCTION` once a `.env.production` exists. Values set in the Vercel dashboard override the file and cannot be read back, which is why the README says to keep them out. And `bun run env:setup --copy` puts the lines for a teammate's `.env.keys` on the clipboard without showing them.

## CI builds with every integration switched off on purpose

The CI behaviour is the part of the environment design that most deserves a second look, because it is counter-intuitive.

CI holds no key. The `ci.yml` workflow deletes the encrypted files before building, so CI builds with every integration off, exactly like a fresh fork. The README states the reasoning: the Vercel preview build is the check with real configuration, and keeping the key out of CI keeps it away from dependency code in Dependabot and in fork pull requests.

That is a security decision with a testing cost, and the project accepts the cost knowingly. The consequence is that a green CI run tells you the project compiles and the core tests pass, but it does not tell you that the Sanity integration still resolves against the current API, because that code path was removed before the build. Integration breakage is caught by the preview deployment instead.

The same reasoning shows up in the handoff story. The client handoff is the repository plus the private key, and the README notes there is no vendor account to transfer. The key travels separately from the code, and the person receiving the project needs both.

For a template, this is a coherent position: the secrets live with the deployment platform, the code lives in git, and neither depends on the other's build step. If you deploy somewhere other than Vercel, you inherit the job of getting the key into that platform, and the README's Vercel-specific path does not cover it.

## One check script stands in for a whole CI pipeline

The `check` script is worth reading as a description of what this project considers done. It runs `oxlint` with unused disable directives reported and zero warnings allowed, then `oxfmt --check`, then a type-aware lint pass, then `ensure:typegen`, then `tsc --noEmit`, then `bun test --parallel`, then the oxlint plugin test, then `manifest:check`, then `check:assets`, and finally `check:disables`.

Several of those deserve comment. `ensure:typegen` tests for `next-env.d.ts` and the generated route and root params type files, and runs `next typegen` if they are missing, so type checking does not silently skip the App Router's generated types. `check:disables` is the counterpart to the `--report-unused-disable-directives` lint flag, catching the lint suppressions that are no longer needed. `manifest:check` ties into the `manifest.ts` route in the app directory. The zero-warnings setting on both lint passes means a warning fails the check, which is stricter than most starters.

The repository layout backs this up with more machine-readable guidance. There is `AGENTS.md`, described as the canonical rules for all AI tools and contributors, `ARCHITECTURE.md` for key decisions, patterns and customization, `COMPONENTS.md`, `SECURITY.md` and `THIRD-PARTY-NOTICES.md`. There is also a `doctor.config.ts` and a `.react-doctor/` directory, a `lint-disables.json`, and `proxy.ts` with its own `proxy.test.ts`.

The `AGENTS.md` file is a signal about how the project expects to be worked on. Conventions that live in a file every tool and contributor reads, rather than in a wiki, are the part of a starter that keeps paying off after the clone.

## What a starter cannot hand you, and when to skip it

The honest limits of this project are the usual limits of a template, plus two specific ones.

The first is that you still have to make it yours. The repository's own answer is the setup script and the manual, and both help, but a starter cannot know your content model, your type scale, your deployment constraints or your performance budget. The Lighthouse and navigation benchmark scripts are there so you can measure rather than guess, which is the right posture.

The second specific limit is the toolchain. Bun is the runtime, the package manager and the test runner, and oxlint and oxfmt replace the ESLint and Prettier pair. That is a coherent modern stack, and the README explains why, but it does mean a team whose CI or editor setup is built around Node and npm has work to do, and that a fork will diverge from the upstream template quickly, which reduces the value of later pulling in upstream improvements. The `bun run setup:project` trimming compounds this: every fork's dependency graph is different, so template updates are a merge exercise rather than a pull.

The encrypted env model is the third. It is a good fit for a studio that hands projects to clients and wants the key to travel with the repository, and a poor fit for an organisation with an existing secrets platform, a policy of never storing configuration in the repository, or a deployment target that cannot be given a build-time key. The `.env` file is committed, encrypted, and every value in it including public ones. For most people that is fine, and for some compliance regimes it is not.

The alternative approach, for comparison, is starting from a bare `create-next-app` and adding only what you need. The difference in approach is that Satus front-loads decisions for you, the toolchain, the styling contract, the SEO and AEO surfaces, the env handling and the check script, while the bare start leaves all of it to you. If your project is a small site with a simple content model, the bare start is less total work. If it is a visual, content-driven site where you want the WebGL pieces available but optional, Satus is the smaller commitment.

## Conclusion

Adopt Satus if you are starting a content-driven marketing site or a portfolio on the App Router and you want the 3D and WebGL pieces to be opt-in rather than a tax on every page, since the setup script removes what you do not use. Do not adopt it if you are on a team that cannot run Bun, if you want a batteries-included template with content already loaded, or if your deployment platform cannot accept an encrypted env file that needs a key at build time. Verify three things first: that Node 24.20 or newer and Bun 1.4.0 or newer are available on every machine that contributes, that the `bun run env:setup` step succeeds against your linked Vercel project before you rely on any integration, and that the base URL your share cards and sitemap use matches the domain you actually want indexed, since the default comes from the platform rather than from your file.

## FAQ

### What stack does the Satus Next.js starter use?

It is built on Next.js 16 with React 19 and the App Router, Tailwind CSS v4 alongside CSS Modules, strict TypeScript, and Bun as the runtime, package manager and test runner, with oxlint and oxfmt for linting and formatting.

### How do I start the Satus starter locally?

You need Node.js 24.20 or newer and Bun 1.4.0 or newer, then run `bun install` and `bun dev` and open localhost:3000, where the landing page is a step-by-step manual for taking the clone to a shippable site.

### How does Satus handle environment variables?

The `.env` file is committed but encrypted with dotenvx, and every value in it is encrypted. The first `bun dotenvx set` creates the file and its own key, `lib/env.ts` throws on values it cannot decrypt, and the private key lives in `.env.keys` at the repo root, which is git-ignored.

### Does CI in Satus have access to the environment variable key?

No. The `ci.yml` workflow holds no key and deletes the encrypted files before building, so CI builds with every integration off, like a fresh fork. The Vercel preview build is the check that runs with the real configuration.

### How do I remove the integrations I do not need in Satus?

Run `bun run setup:project`, which strips unused integrations interactively, removing their code, dependencies and env vars. Sanity, Shopify, HubSpot and Mailchimp sit under `lib/integrations`, and WebGL under `lib/webgl` behind the feature flags in `lib/features`.

## Sources

- [darkroomengineering/satus on GitHub](https://github.com/darkroomengineering/satus)
- [License: MIT](https://github.com/darkroomengineering/satus/blob/main/LICENSE)
- [Project website](https://satus.darkroom.engineering)
- [README](https://github.com/darkroomengineering/satus/blob/main/README.md)
- [Releases](https://github.com/darkroomengineering/satus/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/darkroomengineering-satus
