This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
npm start # Start development server (NODE_ENV=development, https://dev.api.citizenos.com:3003)
npm test # Run full Mocha test suite (NODE_ENV=test)
npm run eslint # Lint via Grunt/ESLint
npm run dbmigrate # Run Sequelize DB migrations
npm run dbrollback # Undo last migration
npm run dbstatus # Check migration status
npm run dbcreate # Create DB schema from scratch (requires DATABASE_URL env var)Run a single test file or grep specific tests:
NODE_ENV=test NODE_TLS_REJECT_UNAUTHORIZED=0 ./node_modules/.bin/mocha --ui tdd --no-exit test/api/topic.js
NODE_ENV=test NODE_TLS_REJECT_UNAUTHORIZED=0 ./node_modules/.bin/mocha --ui tdd --no-exit --grep "POST /api/v1/topics" test/api/topic.jsapp.js is the entry point. It follows a dependency injection via app.set/get pattern — every shared object (models, logger, config, libraries, middleware) is stored on the Express app instance and retrieved with app.get('key') inside route/service modules.
All route and service modules export function(app) { ... } and are auto-loaded at startup:
routes/api/*.js— public REST API endpointsroutes/api/internal/*.js— internal endpoints (API key auth only)services/*.js— shared business logic used by multiple route files
Every response goes through libs/middleware/response.js, which adds these helper methods to res:
res.ok(msg?, statusCode?, data)— 200res.created(msg?, statusCode?, data)— 201res.badRequest(msg?, statusCode?, errors)— 400res.unauthorised(msg?)— 401res.forbidden(msg?)— 403res.notFound(msg?)— 404res.internalServerError(msg?)— 500
Response body shape:
{ "status": { "code": 20000 }, "data": { ... } } // success
{ "status": { "code": 40000, "message": "..." }, "errors": { ... } } // errorStatus code is a 5-digit number: HTTP code × 100 + optional sub-code.
Passport.js with strategies in libs/passport/index.js:
- Local (email/password)
- Google OAuth2 (
passport-google-oauth20) - Facebook (
passport-facebook) - Estonian e-ID: Smart-ID (
libs/) and Mobiil-ID (libs/) — REST clients, initialized inapp.js
Auth token parsing for API calls is in libs/middleware/authTokenParser.js. loginCheck middleware enforces authentication and optional scope checks. authTokenRestrictedUse handles limited-purpose JWT tokens (e.g. invite links).
Sequelize 6 with PostgreSQL. All models live in db/models/. The index (db/models/index.js) auto-loads every model file and sets up associations.
Key domain models: Topic, Group, User, Vote, VoteOption, Comment, Discussion, Ideation, Idea, Attachment, Activity.
Join/membership models follow the pattern {Entity}Member{User|Group} (e.g. TopicMemberUser, GroupMemberUser).
Models prefixed with _ (e.g. _TopicInvite, _GroupInvite) are base/abstract models not used directly.
All models use paranoid: true (soft delete via deletedAt).
Use the asyncMiddleware wrapper (app.get('middleware.asyncMiddleware')) for async route handlers so unhandled rejections are forwarded to Express error handling:
router.get('/path', asyncMiddleware(async (req, res) => { ... }));Uses node-config. Priority order: config/default.json → config/{NODE_ENV}.json → config/local.json → env vars (see config/custom-environment-variables.json).
Create config/local.json from config/default.json for local overrides — it is .gitignored.
Configured in config/*.json under services.*:
- Etherpad: collaborative document editing, client at
libs/cosEtherpad.js - Redis: rate limiting and session store (
libs/redis/), supportsREDIS_TLS_URL/REDIS_URLenv vars - S3: file storage (
libs/cosS3.js), used whenconfig.storage.type === 's3' - Email: Campaign + Nodemailer via
libs/email.jsandlibs/campaign/emailClient.js; templates inviews/emails/with local overrides inconfig/emails/
rateLimiter(allowedRequests, blockTime, skipSuccess) and speedLimiter(...) are set on app and consumed by individual routes. Both are no-ops in test environment unless ENABLE_RATE_LIMIT=true.
Tests use Mocha (TDD UI) + Chai + Supertest. Each test file in test/api/ matches a route file. Tests define helper functions like _topicCreate(agent, ...) (which assert a specific HTTP code) and topicCreate(agent, ...) (which defaults to the happy-path HTTP code). Run the full suite before committing.
Conventional Commits: type(scope): description — e.g. feat(topic): add co-author support.