Decisions
One entry per decision that can be seen in the code, the README or
DEPLOYING.md. The repository's history starts with a single commit, "Initial
open-source release" (2026-10-13), so most entries carry that date: it is when
the decision was recorded, not necessarily when it was first made. Where the
code does not say what else was considered, the entry says so.
1. One Next.js server with MongoDB
- Date: 2026-10-13
- Decided: The web pages and the JSON API are one Next.js 16 app (App
Router route handlers), running as a single long-lived Node server, with
MongoDB through Mongoose for structured data.
- Options weighed:
DEPLOYING.md rules out platforms that run code in
throwaway, read-only functions. No alternative to MongoDB is recorded.
- Why: Repositories are files on the server's disk, so the server has to
live as long as the data does. One process keeps the stack small to run.
2. Real git repositories on disk, written with isomorphic-git
- Date: 2026-10-13
- Decided: Every repository is a git repository on local disk at
<owner>/<repo>.git, read and written with isomorphic-git.
- Options weighed: Shelling out to the
git binary (src/lib/diff.ts
notes it works "without shelling out"). The git smart-HTTP protocol is not
built; the README says plain git clone and git push are not supported yet.
- Why: Pure JavaScript runs on any Node host, and the result is real git
history that can be merged, diffed and exported.
3. Local disk is a cache, a bucket snapshot is the backup
- Date: 2026-10-13
- Decided: After every write, snapshot the repository as a tar.gz to any
S3-compatible bucket. Before any route reads a repository,
repoAccess
restores it from the snapshot if the files are missing.
- Options weighed: A persistent volume alone (still supported); making
object storage the source of truth (named as the next stage in
src/lib/storage/backup.ts); the tar binary versus the pure-JS tar
package.
- Why: Many hosts give a container a fresh filesystem on restart. Backups
used to be write-only, so they were connected to a restore path inside the
one function every repository route already calls. Pure-JS tar avoids
silent backup failures on images without the binary.
4. Run one instance, keep locks and limits in memory
- Date: 2026-10-13
- Decided: Per-repository write locks, rate limits and in-flight restores
live in process memory. Operators run exactly one instance.
- Options weighed: A shared store such as Redis or MongoDB, named in the
comments of
src/lib/locks.ts and src/lib/ratelimit.ts as a later stage.
- Why: It needs no extra service and is correct for one instance.
DEPLOYING.md says to scale up, not out.
5. Username and password sign-in, with a required email
- Date: 2026-10-13
- Decided: NextAuth v5 with a credentials provider (username + bcrypt
password, cost 12) and JWT sessions. Email is required at sign-up and is
used for one-time codes.
- Options weighed: An optional email (the sign-up route explains why it is
now required). No OAuth provider is configured.
- Why: On a username-only account, a forgotten password could never be
recovered without an address to send a code to.
6. Bearer tokens for the CLI and MCP, revocable by password reset
- Date: 2026-10-13
- Decided:
nucleus login hands off to the browser, which redirects to
127.0.0.1 with a 30-day JWT. Tokens carry the account's tokenVersion; a
password reset increments it.
- Options weighed: Plain self-contained JWTs, which are fast but cannot be
withdrawn (
src/lib/tokenversion.ts).
- Why: A reset usually follows losing control of an account, and a 30-day
token should not outlive it.
7. One-time codes hashed with HMAC
- Date: 2026-10-13
- Decided: 6-digit codes from
crypto.randomInt, stored only as an HMAC
peppered with AUTH_SECRET, bound to one purpose, valid 10 minutes and 5
guesses, one live code per purpose.
- Options weighed: bcrypt for the code hash, rejected in
src/lib/otp.ts
as added latency with no real gain for a short-lived, rate-limited code.
- Why: A database leak must not hand out working reset codes, and reset
responses must not reveal which accounts exist.
8. Email through one replaceable function
- Date: 2026-10-13
- Decided: All mail goes through
sendMail() in src/lib/mail.ts, which
calls Harakumo Mail with one API key.
- Options weighed: Any other provider;
DEPLOYING.md says to replace that
one function.
- Why: One small seam keeps the provider swappable.
9. One authorization function for every repository route
- Date: 2026-10-13
- Decided:
repoAccess() in src/lib/authz.ts decides read, contribute,
write and admin for every repository route, and answers 404 for a private
repository the caller cannot see.
- Options weighed: Owner checks written per route. The repository detail
route notes its old hand-rolled check locked collaborators out.
- Why: The rules stay in one place, and private repositories stay
undisclosed.
10. Fail closed on missing secrets in production
- Date: 2026-10-13
- Decided: In production, Nucleus refuses to start without
MONGODB_URI,
AUTH_SECRET and SECRETS_ENCRYPTION_KEY, and the modules that use them
refuse to fall back to development values.
- Options weighed: The earlier fallbacks (a signing secret committed to the
source, a localhost database); an import-time check, rejected because
next build imports every module on a machine with no secrets.
- Why: A public default secret would let anyone sign a token for any
account. A server that dies at boot is easier to diagnose than one that
returns errors.
11. A separate key for repository secrets
- Date: 2026-10-13
- Decided: Repository secrets are encrypted with AES-256-GCM using
SECRETS_ENCRYPTION_KEY. Older AES-256-CBC values still decrypt.
- Options weighed: Reusing
AUTH_SECRET, rejected in src/lib/crypto.ts;
CBC, replaced by authenticated GCM.
- Why: The two keys rotate for different reasons, and rotating the session
key must not destroy stored secrets. GCM detects tampering.
12. Two AI providers behind one function, bring your own key
- Date: 2026-10-13
- Decided: Every AI feature calls
runAI() in src/lib/ai.ts, which
supports Claude (Anthropic) and Grok (xAI), chosen per request, and falls
back to the other only when the chosen one has no key.
- Options weighed: Calling provider SDKs from each feature, which the file
header rules out.
- Why: Adding or swapping a provider is a change in one file, and each
operator pays for their own usage.
13. AI output as file blocks in plain text
- Date: 2026-10-13
- Decided: The model returns files as
===FILE: path=== blocks and
deletions as ===DELETE: path===; agents use similar ===TASKS===,
===ISSUES=== and ===COMMS=== blocks. The server parses them.
- Options weighed: Not recorded in the code.
- Why: Not recorded in the code.
14. Three kinds of live preview
- Date: 2026-10-13
- Decided: Static sites are served from the repository with a sandbox
Content Security Policy.
package.json projects run in a WebContainer in
the visitor's browser. Other languages run in a Docker container only when
a Docker socket is available.
- Options weighed: Running every preview on the server, which costs CPU
and needs a Docker socket most managed hosts do not provide.
- Why: Browser previews cost the server nothing, and the sandbox stops a
user's JavaScript from acting on the Nucleus origin.
15. Pushes merge instead of overwrite, and never delete by accident
- Date: 2026-10-13
- Decided: Clients send the commit they last synced. The server does a
file-level three-way merge and returns conflicts instead of guessing. Files
are deleted only when the client sends the complete tree and skipped
nothing. Binary files travel as base64.
- Options weighed: Taking every upload as authoritative; treating a file
missing from the upload as deleted, which
src/lib/safepush.ts and
tests/push-integrity.test.mts record as the cause of pushes deleting a
site's own images.
- Why: A teammate's work must not be silently overwritten, and a push must
not remove files the user did not remove.
16. Size limits on pushes
- Date: 2026-10-13
- Decided: 25 MB per file, 100 MB per push, 500 MB per repository, 20,000
files per push, with errors that say what to do.
- Options weighed: No ceiling, rejected in
src/lib/quota.ts.
- Why: The disk is shared by every repository on a single instance.
17. Trust X-Forwarded-For only when told to
- Date: 2026-10-13
- Decided: Rate limits read the client IP from
X-Forwarded-For only when
TRUST_PROXY=true, and then take the last hop. Per-IP sign-in limits apply
only in that case.
- Options weighed: Always trusting the header (clients could reset their
own limits), or always limiting per IP (without a proxy every caller is one
identity, and ten failures would lock every account).
- Why: Correct behaviour both behind a proxy and directly exposed.
18. Type check as its own step
- Date: 2026-10-13
- Decided:
next build skips type checking (ignoreBuildErrors), and
npm run typecheck runs tsc --noEmit separately.
- Options weighed: Type checking inside the build.
- Why: Keeps the build short on small hosted builders.
19. A Docker image that runs anywhere
- Date: 2026-10-13
- Decided: A multi-stage Node 22 image that runs as a non-root user, keeps
repositories on a volume at
/app/repos, and has a health check that
requires a connected database and a writable repository directory.
- Options weighed: Running without Docker is documented in
DEPLOYING.md.
- Why: Any machine with Docker can host it, and orchestrators can tell
"started" from "actually serving".
20. Open source under MIT, separate from the hosted service
- Date: 2026-10-13
- Decided: Nucleus is published under the MIT license as a self-hostable
project. Harakumo's hosted code home grew out of it and is a separate
service.
- Options weighed: Not recorded.
- Why: The README describes it as "a self-hostable code platform"; the
license lets anyone run and change it.
21. Vendor-neutral configuration
- Date: 2026-10-13
- Decided: Storage is any S3-compatible bucket set by
STORAGE_ENDPOINT
and three more STORAGE_* variables. Hosting is any machine that runs Node
22 or Docker. Mail is one replaceable function.
- Options weighed: Not recorded.
- Why:
DEPLOYING.md lists a small VPS, a home server or a managed
container host as good fits; nothing in the code ties Nucleus to one
provider.
22. No deployment or custom domains in this release
- Date: 2026-10-13
- Decided: The open-source release hosts code and previews only. It has no
routes for deploying apps or attaching domains.
- Options weighed: Not recorded.
- Why: Not recorded. One trace remains: the owner-only
decrypt_for_deploy action on api/repos/[owner]/[repo]/secrets.
23. Security fixes made before publishing
-
Date: 2026-10-13 (present in the first public commit; the date of each
fix is not recorded)
-
Decided: Fixes whose comments describe the earlier behaviour:
- Tree and blob paths were joined onto the repository directory without a
guard, so
../ could read other files. Both now go through
resolveReadPath, covered by tests/path-traversal.test.mts.
- Token signing fell back to a secret committed to the source; it now fails
closed in production.
- A missing
MONGODB_URI fell back to localhost in production; it now
fails.
- Bearer tokens could not be withdrawn;
tokenVersion now revokes them on a
password reset.
- Binary files were read and pushed as UTF-8 and files missing from a push
were deleted; both are fixed and covered by
tests/push-integrity.test.mts.
- The repository detail route used its own owner check; it now uses
repoAccess.
Safeguards whose comments describe the attack they block:
- The CLI login
port must be digits in 1024 to 65535, so a value like
80@host cannot send the token to another host.
- Repository list and detail responses exclude secrets and collaborators.
- User files served for static previews carry a sandbox CSP.
- Password-reset requests answer the same way whether or not the account
exists, and send mail in the background so timing does not tell.
- Forking requires read access, so a private repository cannot be copied
into a repository the caller controls.
- Starting a container preview requires write access, because it runs the
repository's code on the server.
-
Options weighed: Not applicable; each is a correction or a guard.
-
Why: These protect accounts, private code and the server once the code
and the instance are public.