Todo app — Express.js 5 + TypeScript (ESM) + EJS, data centralized in
MongoDB via Mongoose ODM (single Todo schema), with per-account
authentication (Better Auth: email + password, device & auth-activity tracking).
pnpm install
pnpm dev # development mode (node --watch src/index.ts)
pnpm build # compile TS to dist/
pnpm start # run from dist/ (production)You need a running MongoDB (mongod locally, Docker, or Atlas). Default port
3000; set via env PORT. Optional env vars:
MONGODB_URI (connection, default mongodb://127.0.0.1:27017/planner),
TODOS_LIMIT (max number of todos, default 1000), SITE_URL (absolute
domain for canonical/sitemap), BETTER_AUTH_URL (public origin for auth
cookies), BETTER_AUTH_SECRET (required in production — generate one
with openssl rand -base64 32).
src/
app.ts middleware assembly + routes
index.ts MongoDB connection → listen + graceful shutdown
config/app.ts env config (PORT, MONGODB_URI, SITE_URL, auth)
db/connect.ts connection bootstrap (exit when DB is unreachable)
routes/ per-domain routers (todo, seo, auth, main, health, not-found)
app/auth/ Better Auth instance + device/IP tracking + auth audit
app/middleware/ requireAuth gate (session check → /login)
app/controllers/ thin layer: parse request → call service → render
app/services/ business logic + all MongoDB access (ODM)
app/models/ Mongoose schemas (Todo, DeviceLog) + document mapping
app/helpers/ render (meta/canonical/asset version), date, API response
views/ EJS (layouts, partials, pages: login/register/account)
logger/ mini logger (info/warn/error + timestamp)
interfaces/ Todo type
- RESTful routes:
GET /,POST /,GET /add-todo,GET /edit-todo/:id,POST /toggle/:id,PUT /(update),DELETE /(delete),POST /restore(undo). - Account routes:
GET/POST /register,GET/POST /login,POST /logout,GET /account(account details, change password, login devices + auth activity, revoke sessions). Todo pages and the/apibackup endpoints require a session. - Backup routes:
GET /api/export(download JSON),POST /api/import(replace data). - Design tokens: self-hosted Geist/Geist Mono, neutral Bento zinc/slate,
one desaturated accent. Dark theme persists in
localStorage. - Storage: every mutation is a Mongo operation through Mongoose (the schema
validates the 200-char name, boolean
completed, priority enum, anddue);createdAt/updatedAthandled automatically bytimestamps. - The app never writes data to disk — the server only writes logs.
All data lives in the MongoDB collection (the database on the MONGODB_URI
connection). To keep it safe:
- Download a copy — the download button on the home page or
GET /api/exportproduces aplans.jsonfile with all todos. Store it in a safe place. - Restore — the import button on the home page (pick a
.jsonfile) orPOST /api/importreplaces all current data. Import rejects invalid formats or anything over the limit (MAX_TODOS) without touching old data. - Make sure the database itself is backed up (provider-level backup or point-in-time from MongoDB); the JSON export is an extra safety net you can re-import.
State lives in MongoDB — the server is fully stateless, good enough for a single user/family or many users via a managed DB provider.
- VPS/Railway/Fly: run
pnpm build && pnpm start, setPORT,MONGODB_URI(e.g. Atlas),SITE_URL, andBETTER_AUTH_SECRET; pointGET /api/health-checkat an uptime monitor (e.g. UptimeRobot/Cronitor) — the response includes the DB connection status (data.db).trust proxyis set for one reverse proxy (Nginx/Caddy). - Privacy: accounts protected by Better Auth (email + password, per-login
IP/device records and a full authentication audit trail — sign-up/sign-in
attempts, sign-out, password changes, session revocations with device
context — on the
/accountpage). Data lives in the centralized database — adjust your privacy story accordingly. - Scaling: raise
TODOS_LIMITvia env and use the Mongo indexes defined in the schema if needed; no UI changes required.
helmetenabled: X-Content-Type-Options, CSPdefault-src 'self'+ nonce for the inline theme script, Strict-Transport-Security, Referrer-Policy, andX-Powered-Byoff.Permissions-Policydenies geolocation/camera/microphone.- Sessions via Better Auth: HTTP-only signed cookies (
rencana.*), 7-day expiry + 24h sliding update, per-endpoint rate limits, CSRF-protected auth endpoints, and open-redirect-safe?next=handling. Secrets never committed. - Input validated: name is a required string, trimmed, double spaces
collapsed, max 200 chars;
duechecked asYYYY-MM-DD; priority only low/medium/high. The Mongoose schema repeats those limits as a second layer and rejects out-of-enum priority values. - Body-parser limit 10kb; mutations answered with
Cache-Control: no-store. - No data files in the repo — the app writes only logs; DB credentials go
through the
MONGODB_URIenv, never committed.
pnpm lint # biome check
pnpm typecheck # tsc --noEmit (src + test)
pnpm test # node:test (+ pretest: typecheck runs automatically)
pnpm test:coverage # node:test + coverage report (80% src/ line threshold)
pnpm build # tsc → dist/The test/ suite uses built-in node:test (no extra dependencies) and runs
Node 24 directly on TypeScript requests — covering unit (validator, service,
model schema), HTTP integration (real Express server via fetch), Mongoose
schema defaults & enums, max limits, XSS/escaping, Unicode, the full auth
flow (register/login/logout, protected redirects, device logging, session
revocation), and a production smoke
test. Each test file uses mongodb-memory-server (in-memory MongoDB,
no separate server install) via an internal <uri>; pretest runs typecheck
first; test:coverage fails when src/ line coverage drops below 80%.
Husky pre-commit runs biome + lint-staged; pre-push runs pnpm test.
Commits use conventional style (feat:/fix:/perf:). Release history lives in
CHANGELOG.md; contribution guide in
CONTRIBUTING.md.
Apache-2.0