Imported from valyastriz/BCDefectsAndEnhancements (
.claude/skills/new-project-creation/SKILL.md). Install upstream withnpx skills add valyastriz/BCDefectsAndEnhancements --skill new-project-creation. Copyright stays with the author.
New Project Creation
Purpose
Use this skill when building a new project that should adopt the existing database-manager workflow instead of inventing a new migration system.
This skill makes the agent scaffold the support files, environment, startup flow, and validation steps required for a fresh project to run with a copied database-manager.js and generate-migrations.js. It also provides optional, user-confirmed paths for Redis-based cache invalidation, ws-based live data, and multi-language support for both static UI text and translated dynamic content. When building the database backend, use Sequelize models, associations, and ORM access patterns rather than hardcoded SQL queries.
Use This Skill When
- Creating a brand new Node backend that should use the existing
database-manager.js - Bootstrapping a new Sequelize + PostgreSQL project from scratch
- Reusing the migration and auto-sync system in another repository
- Setting up a fresh project where the only supplied files are or will be (ask the user if they want to provide them)
database-manager.jsandgenerate-migrations.js - Creating a new project that may optionally include Redis auto invalidation,
ws-based live data, and/or multi-language UI with translated dynamic content - Building a project that must match the current migration, cache, and live update architecture instead of replacing it
Do Not Use This Skill When
- The project does not want Sequelize or PostgreSQL
- The project wants manual migration authoring instead of
database-manager - The project wants a different migration architecture unrelated to the copied files
- The project explicitly wants Socket.IO as the primary live-data transport rather than the current
wsimplementation
Behavior
When this skill is active, follow these rules. They are numbered sequentially and grouped: scope and detection (1–7), ask-first gates (8–12), hard security and data rules (13–22), finish-line checks (23–24). LOAD the operating-discipline skill before starting and fill its templates from its text, not from memory.
Scope and detection
- Assume the only guaranteed source files are
database-manager.jsandgenerate-migrations.jsunless the user provides more. - Detect whether those files already exist in the target project before creating support files around them.
- If the two files are not yet present, scaffold the surrounding structure first so they can be copied in without path breakage.
- Do not redesign
database-manager; build the project around its existing import and runtime expectations. - Always create the minimum compatible support files before trying to run migrations.
- Never manually author schema migrations when
database-manageris the intended migration system. Change models and let the manager detect and generate migrations. - Layout decision: use a
backend/folder for a fullstack project. Choose a flat root ONLY if the repo already has server code at its root or the user explicitly asks for it; record the choice with a one-line reason (operating-discipline section 5).
Ask-first gates
- Ask the user before adding Redis support.
- Ask the user before adding live data support.
- Ask the user before adding multi-language support.
- If the user chooses live data (rule 9 governs — always ask first), implement the current
wspattern, not Socket.IO, unless they explicitly ask for a Socket.IO adaptation; build the frontend on the WebSocketContext and wsManager patterns from the start, and keep the frontend PWA-compatible so live data can expand later without a breaking refactor. - If the user chooses multi-language support, implement the current
react-intlfrontend pattern and backend user-language resolution pattern rather than inventing a different i18n stack.
Hard security and data rules
- Scaffold backend code around the shared logger in
backend/functions/logger.jsand make new backend modules use that logger instead of raw console statements. - Any backend code you create must include meaningful structured logs for startup, important background work, guard-path exits, successful mutations, and caught failures.
- Keep generated logs safe and useful: include identifiers and outcome context, but do not log secrets, credentials, tokens, or full sensitive payloads.
- Never authenticate with
jwt.decodeanywhere — websocket and HTTP auth usejwt.verifywith explicit algorithms, audience, and issuer. - Never cache an authenticated response under a tenant-blind key: cache keys include tenant scope and the query string, and invalidation uses tenant-scoped patterns.
- Money is never FLOAT/DOUBLE: any currency-amount column is
DECIMALwith explicit precision. Relations join on ids, never on name strings — a rename must never silently unlink data. - Scaffold every list route paginated by default (
page/limitclamps,{ rows, total }response) — fetch-all list endpoints must not be the starter pattern. - Build the database layer around Sequelize models, associations, scopes, and ORM methods instead of hardcoded SQL strings.
- Raw SQL is allowed only where Sequelize cannot cover the need cleanly — canonical cases: a recursive CTE, window functions over large sets, bulk upsert with
ON CONFLICTarithmetic — and each such query lives behind one named data-access function, never scattered through controllers or services. - Treat destructive database options as opt-in only and keep them disabled until verification passes.
Finish-line checks
- Snippets in this skill are starting points, not literal truth: before finishing, confirm every import in generated files resolves to a file you scaffolded, every referenced function exists, and no placeholder invalidation, stubbed handler, or unwired
res.localscontract remains. - Also scaffold the project's contracts layer: a root
CLAUDE.mdand a rootplan.md(see the sections below) and the frontend locale check scripts with their npm aliases.
STOP — mid-scaffold red flags
Halt and fix (operating-discipline section 9) the moment you catch yourself:
- Authenticating a websocket or route with
jwt.decodeinstead ofjwt.verify. - Writing a cache key without tenant scope or the query string.
- Scaffolding an unbounded
findAlllist route. - Typing FLOAT/DOUBLE for a money column, or joining on a name string where an id exists.
- Copying a snippet from this skill without confirming it matches the source project's current file — the source project wins; surface any divergence as a headline (see the premise check in Inspect First).
Delegation
Per context-lean-orchestrator, scaffolding a project is a job for one strong subagent, not inline orchestrator work. The Required Questions below are answered with the user BEFORE dispatch. The orchestrator writes a brief file in the scratchpad using operating-discipline section 8's BRIEF template, with these fields filled concretely:
- Constraints (VERBATIM): the user's answer to every Required Question, quoted — the subagent never guesses about Redis, live data, or multi-language support.
- Chosen layers: each optional layer marked add / do NOT add — an unchosen layer is explicitly forbidden, not merely omitted.
- You own: the new project's tree. You must NOT touch: the source project (read-only reference) or anything outside the new tree.
- Checklist: the scaffold steps, prune/rename lists, and the End-of-Scaffold Boot Checklist — to be executed, not described.
- Return exactly: operating-discipline's REPORT, plus the boot checklist item-by-item with the command run and observed result, and an explicit "not run:" list.
The orchestrator reviews the returned report against the brief before accepting the work.
Workflow
1. Inspect First
Before writing code, inspect the target project and identify:
- whether the project uses
backend/or a flat root - whether
scripts/database-manager.jsalready exists - whether
scripts/generate-migrations.jsalready exists - whether Sequelize is already configured
- whether there is already a
models/index.js - whether data access is expected to go through Sequelize models and associations rather than raw queries
- whether there is already a logging bridge compatible with
initializeBackendLogging() - whether nearby backend modules already define structured logging patterns worth reusing
- whether frontend live data is needed or backend-only notifications are enough
Record these findings with file references as the Knowns of your INTAKE block (operating-discipline section 1) — inspection that stays in your head is not inspection.
Premise check (operating-discipline section 2): before scaffolding any pattern this skill calls "current" or "house", open the source project the user is copying from and confirm the pattern still exists there in that shape. Where a snippet in this skill diverges from the source project's current files, the source project wins — surface the divergence as a headline in your report; never silently scaffold the stale version. This applies especially to the legacy modelCacheHooks template below: some source projects have retired model-hook invalidation in favor of explicit invalidation in mutation paths. If the source project cannot be inspected (files pasted, no repo access), the default is NO modelCacheHooks — invalidate explicitly in mutation paths; scaffold the legacy file only on positive confirmation that the source still uses it.
2. Ask The Required Questions
Before adding optional systems, ask the user these concrete questions:
- Do you want Redis support for response caching and automatic invalidation?
- Do you want live data support using the current
ws-based WebSocket pattern? - Do you want multi-language support using the current
react-intlfrontend pattern and backend translation helpers?
Recommended Redis choices:
- no Redis
- Redis cache only
- Redis cache plus background invalidation queue workers
Recommended live-data choices:
- no live data
- backend-only websocket notifications
- full websocket live data with frontend manager and provider
Recommended multi-language choices:
- no multi-language support
- static UI translations only
- UI translations plus persisted user language preference
- full multi-language support with UI translations and backend dynamic-content translation
Record each answer and each structural choice (layout, chosen layers) as a one-line DECISION with its reason (operating-discipline section 5) — in the delegation brief when delegated, in your working notes when inline. A layer the user did NOT choose is recorded as forbidden ("do NOT add"), not merely left unchosen.
3. Scaffold Base Migration Support
Create the minimum files required to support the copied migration system:
backend/scripts/database-manager.jsbackend/scripts/generate-migrations.jsbackend/models/index.jsbackend/config/config.jsbackend/config/dependencyOrder.jsbackend/functions/logger.jsbackend/migrations/backend/package.jsonscripts or rootpackage.jsonscriptsbackend/start.shor equivalent startup hook
4. Wire Startup In The Correct Order
The base startup order should be:
- initialize logging
- load environment
- initialize Sequelize
- run
database-manager - start the HTTP server
- optionally initialize Redis
- optionally start queue workers
- optionally attach websocket server and listeners
The HTTP server is guarded from the first route: scaffold middleware/authMiddleware.js (verify JWTs with jwt.verify + JWKS, explicit algorithms/audience/issuer — the same options as the websocket auth snippet, NEVER jwt.decode) mounted on all /api routes, and middleware/permissionMiddleware.js exporting a requirePermission("<resource>", "<action>") stub that every route registers (or a // public: <reason> comment). Scaffold both BEFORE the first route file so later routes inherit the guard — a project whose REST routes ship open fails the boot checklist.
5. Scaffold the Project Contracts (CLAUDE.md + plan.md)
Every new project gets a root CLAUDE.md so the shared skills have their per-project grounding layer. Model it on the source project's CLAUDE.md, filled in with the new project's concrete names:
- Definition of Done (runnable verification checklist with this project's commands and ports)
- Live-update contract (this project's ws helper and frontend subscription layer, tenant filter requirement)
- Migration workflow (this project's commands; whether migrations auto-run on boot)
- Backend + frontend conventions (layout, permission middleware, pagination default, logger, theme, snackbar/dialog components)
- Localization workflow (locale set,
locales:check/locales:clean) - Docs & guided-tour upkeep: if/when the project has an in-app help-docs module or guided tours, keeping them current for a changed feature is part of the Definition of Done (see guided-onboarding-walkthroughs)
- AI usage guidance (match model/effort to task complexity)
Also scaffold a root plan.md from the project-plan-maintenance skill's skeleton: overview, architecture, chosen optional layers, and each Required-Question decision with its one-line reason. The scaffolded CLAUDE.md and plan.md carry the durable decisions; the brief or working notes carry the rest.
6. Verify Before Expanding
Before enabling destructive flags or optional features:
- run the manager in safe diff mode
- verify DB connection and model loading
- confirm
SequelizeMetacan be created - confirm migration output is written to
migrations/ - confirm checksum and schema diff behavior works
- if multi-language support is enabled, verify locale fallback to English, saved user language hydration, and translation-aware response serialization
7. End-of-Scaffold Boot Checklist (all required)
The scaffold is not done until each of these was actually executed:
npm installcompletes cleanly in every scaffolded package- every import in generated files resolves; no placeholder invalidation or stubbed handler remains
database-manager.jsruns clean and the backend boots without require errors- an unauthenticated request to a protected route returns 401, and a request with a valid token succeeds — executed, not assumed
- if live data: the websocket authenticate round-trip succeeds with a real verified JWT, and a second client receives a mutation event without refresh
- if Redis: observe cache MISS → HIT → invalidate on a real endpoint, and confirm the app degrades gracefully with Redis stopped
- if multi-language:
locales:checkandlocales:cleanrun clean
Then run the operating-discipline §6 critique pass on the scaffolded tree before reporting. Report the checklist item-by-item per operating-discipline §6–7 — the command run and the observed result; items that could not run (e.g., Docker unavailable) go under an explicit "not run:" heading with the reason; never summarize as "all checks pass".
Preferred Structure
Prefer this structure for a new fullstack project:
project-root/
backend/
package.json
.env
start.sh
server.js
config/
config.js
dependencyOrder.js
redisConfig.js
queueConfig.js
functions/
logger.js
cacheUtils.js
appLanguages.js
chatTranslation.js
modelCacheHooks.js # legacy — only if the source project still uses it
queueWorkers.js
webSocketUtils.js
middleware/
authMiddleware.js # HTTP jwt.verify (JWKS) — mounted on all /api routes
permissionMiddleware.js # requirePermission("<resource>", "<action>")
cacheMiddleware.js
models/
index.js
User.js
...
migrations/
scripts/
database-manager.js
generate-migrations.js
frontend/
src/
layout/
UserSettingsLoader.jsx
utils/
locales/
wsManager.js
contexts/
ConfigContext.jsx
WebSocketContext.jsx
ui-component/
Locales.jsx
If the project uses a flat root instead of backend/, preserve the same relative import relationships expected by the copied files.
Base Blueprint
These files are the minimum compatibility layer for database-manager.
config/config.js
This file must configure Sequelize for PostgreSQL and support both a single connection URL and discrete DB credentials.
require("dotenv").config();
const useDatabaseUrl = process.env.USE_DATABASE_URL === "true";
const baseConfig = useDatabaseUrl
? {
use_env_variable: "DATABASE_URL",
dialect: "postgres",
dialectOptions:
process.env.NODE_ENV === "production"
? { ssl: { require: true, rejectUnauthorized: false } }
: {},
}
: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST || "localhost",
port: parseInt(process.env.DB_PORT || "5432", 10),
dialect: "postgres",
};
module.exports = {
development: baseConfig,
test: baseConfig,
production: baseConfig,
};
functions/logger.js
This file must expose initializeBackendLogging() because the copied scripts call it at startup.
let loggingInitialized = false;
const logMessage = (message, level = 3) => {
const currentLevel = parseInt(process.env.LOG_MODE || "3", 10);
if (level <= currentLevel) {
console.log(message);
}
};
const logCriticalError = (...args) => {
console.error(...args);
};
const logStructuredMessage = (...args) => {
console.log(...args);
};
const initializeBackendLogging = () => {
if (loggingInitialized) {
return;
}
loggingInitialized = true;
};
module.exports = {
initializeBackendLogging,
logMessage,
logCriticalError,
logStructuredMessage,
};
config/dependencyOrder.js
This file must exist even if it starts empty.
module.exports = [];
models/index.js
This file must load all models and export both the Sequelize instance and the model registry.
const fs = require("fs");
const path = require("path");
const { Sequelize, DataTypes } = require("sequelize");
const env = process.env.NODE_ENV || "development";
const config = require("../config/config")[env];
const sequelize = config.use_env_variable
? new Sequelize(process.env[config.use_env_variable], config)
: new Sequelize(config.database, config.username, config.password, config);
const db = {};
const basename = path.basename(__filename);
fs.readdirSync(__dirname)
.filter((file) => file !== basename && file.endsWith(".js"))
.forEach((file) => {
const defineModel = require(path.join(__dirname, file));
const model = defineModel(sequelize, DataTypes);
db[model.name] = model;
});
Object.values(db).forEach((model) => {
if (typeof model.associate === "function") {
model.associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;
package.json Scripts
At minimum, add these scripts:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"db:migrate": "sequelize-cli db:migrate",
"db:migrate:undo": "sequelize-cli db:migrate:undo",
"db:migrate:status": "sequelize-cli db:migrate:status",
"migration:create": "node scripts/generate-migrations.js",
"migration:run": "node scripts/database-manager.js"
}
}
Add "migration:health": "node scripts/health-check.js" only if you also copy scripts/health-check.js from the source project — do not map migration:health to database-manager.js (they are different tools in the source repo).
start.sh
Run the manager before starting the server when migrations are enabled.
#!/bin/sh
set -e
if [ "$RUN_MIGRATIONS" != "false" ]; then
node scripts/database-manager.js
fi
node server.js
Environment
Base Database Variables
DATABASE_URLUSE_DATABASE_URLDB_HOSTDB_PORTDB_NAMEDB_USERDB_PASSWORDNODE_ENVRUN_MIGRATIONSUSE_INTERNAL_MIGRATOR
Migration Strategy Variables
AUTO_MIGRATION_STRATEGYDB_MIGRATION_PRESETDB_SCHEMA_DIFFDB_SCHEMA_DIFF_DRY_RUNDB_SCHEMA_DIFF_DIRDB_INDEX_DEBUGDB_INDEX_CLEANUPDB_DROP_EXTRADB_DROP_TABLESDB_ORPHAN_NULLIFYMIGRATION_DROP_EXTRA_COLUMNSLOG_MODE
Legacy Compatibility Aliases
If porting older environment files, the manager also recognizes these names as fallbacks:
SCHEMA_DIFFSCHEMA_DIFF_DRY_RUNAUTO_REDUNDANT_INDEX_CLEANUPAUTO_DROP_EXTRA_COLUMNSAUTO_ORPHAN_NULLIFYDEBUG_INDEX_CLEANUP
Recommended Safe Defaults
USE_DATABASE_URL=false
DB_HOST=localhost
DB_PORT=5432
DB_NAME=app_db
DB_USER=postgres
DB_PASSWORD=postgres
RUN_MIGRATIONS=true
USE_INTERNAL_MIGRATOR=false
AUTO_MIGRATION_STRATEGY=inplace
DB_MIGRATION_PRESET=dev
DB_SCHEMA_DIFF=true
DB_SCHEMA_DIFF_DRY_RUN=true
DB_INDEX_CLEANUP=true
DB_DROP_EXTRA=false
DB_DROP_TABLES=false
DB_ORPHAN_NULLIFY=false
LOG_MODE=3
Optional Multi-Language Support
Only add this section after the user confirms they want multi-language support.
The house pattern has two layers:
- static UI translations in the frontend using
react-intl - dynamic content translation in the backend using user language preferences and provider-backed translation helpers
The backend translation files below (appLanguages.js, chatTranslation.js) are new-file templates this skill creates — they are not files copied from an existing project. Do not replace this stack with a different i18n approach unless the user explicitly asks for one.
Supported Languages
Ask the user which languages the app needs (their market decides the list); only if they defer, use this default set:
enesfrroruukzh
What To Build
For static UI translations, create:
backend/functions/appLanguages.jsfrontend/src/utils/locales/en.jsonfrontend/src/utils/locales/es.jsonfrontend/src/utils/locales/fr.jsonfrontend/src/utils/locales/ro.jsonfrontend/src/utils/locales/ru.jsonfrontend/src/utils/locales/uk.jsonfrontend/src/utils/locales/zh.jsonfrontend/src/ui-component/Locales.jsxfrontend/src/contexts/ConfigContext.jsxfrontend/src/layout/UserSettingsLoader.jsxfrontend/src/layout/MainLayout/Header/LocalizationSection/index.jsxor an equivalent language selector- the support files the snippets import:
frontend/src/config.js,frontend/src/hooks/useConfig.js,frontend/src/hooks/useLocalStorage.js,frontend/src/api/userSettings.js(every import in a scaffolded file must resolve) frontend/scripts/check-missing-locale-keys.jsandfrontend/scripts/dedupe-locales.js(copy from the source project), wired as npm aliases:"locales:check": "node scripts/check-missing-locale-keys.js","locales:clean": "node scripts/dedupe-locales.js"
For persisted user language preferences, also add:
- a
User_Settingsmodel or equivalent table storingname=languageandvalue=<code> - API endpoints to fetch and bulk save the current user's settings
For dynamic translated content, also add:
backend/functions/chatTranslation.js- provider manager support for one of
grok,openai, oranthropic - translation-aware model fields such as
original_message_text,translated_texts,sender_language, andtranslation_statuson translated message-like records
Multi-Language Variables
Frontend and persistence support does not require special env vars beyond the normal app config.
If dynamic provider-backed translation is enabled, support provider keys such as:
GROK_API_KEYOPENAI_API_KEYANTHROPIC_API_KEY
The exact names must match what the scaffolded (or copied) provider manager actually reads — verify against its code, never against this list alone. At least one provider must be configured when dynamic translation is requested.
Multi-Language Dependencies
For the frontend locale layer, install:
react-intl
For dynamic backend translation, keep or add:
- a provider manager abstraction
- whichever AI SDKs are required by the chosen providers
Multi-Language Implementation Steps
- Ask the user whether they want UI locale support only, persisted user language preferences, or full dynamic content translation.
- Create
backend/functions/appLanguages.jswith the shared supported language list. - Set frontend config default
i18ntoen. - Create
frontend/src/contexts/ConfigContext.jsxbacked by local storage so language selection persists in the browser. - Create
frontend/src/ui-component/Locales.jsxwithIntlProviderand merge the active locale dictionary over English defaults. - Add locale JSON files under
frontend/src/utils/locales/using flat translation keys. - If user preference persistence is requested, add
UserSettingsLoader.jsxto fetch the savedlanguagepreference after login and apply it to ConfigContext. - Add a language selector UI that updates the local config immediately and persists
[{ name: "language", value: lng }]to the backend when the user is authenticated. - If dynamic translated content is requested, add
backend/functions/chatTranslation.jswithnormalizeLanguage(),getUserLanguage(),getUsersLanguages(), andtranslateTextToLanguages(). - For translated dynamic records, store both the original source text and a
translated_textsmap keyed by normalized language code. - Resolve outgoing content per user by loading their saved language and selecting the best translation before sending the response.
- Use English as the default fallback language everywhere.
Crucial Multi-Language Code
backend/functions/appLanguages.js
const SUPPORTED_APP_LANGUAGES = ["en", "es", "fr", "ro", "ru", "uk", "zh"];
function getSupportedAppLanguages() {
return [...SUPPORTED_APP_LANGUAGES];
}
module.exports = {
SUPPORTED_APP_LANGUAGES,
getSupportedAppLanguages,
};
frontend/src/ui-component/Locales.jsx
import { useMemo } from "react";
import { IntlProvider } from "react-intl";
import useConfig from "hooks/useConfig";
import enMessages from "utils/locales/en.json";
import esMessages from "utils/locales/es.json";
const localeMessageMap = {
en: enMessages,
es: esMessages,
};
function getLocaleMessages(locale) {
return {
...enMessages,
...(localeMessageMap[locale] || enMessages),
};
}
export default function Locales({ children }) {
const {
state: { i18n },
} = useConfig();
const messages = useMemo(() => getLocaleMessages(i18n), [i18n]);
return (
<IntlProvider locale={i18n} defaultLocale="en" messages={messages}>
{children}
</IntlProvider>
);
}
frontend/src/contexts/ConfigContext.jsx
import config from "config";
import { createContext, useMemo } from "react";
import { useLocalStorage } from "hooks/useLocalStorage";
export const ConfigContext = createContext(undefined);
export function ConfigProvider({ children }) {
const { state, setField } = useLocalStorage("app-config", config);
const value = useMemo(() => ({ state, setField }), [state, setField]);
return (
<ConfigContext.Provider value={value}>
{children}
</ConfigContext.Provider>
);
}
frontend/src/layout/UserSettingsLoader.jsx
import { useEffect } from "react";
import useAuth from "hooks/useAuth";
import useConfig from "hooks/useConfig";
import { getMyUserSettings } from "api/userSettings";
export default function UserSettingsLoader() {
const { isLoggedIn } = useAuth();
const { setField } = useConfig();
useEffect(() => {
if (!isLoggedIn) return;
getMyUserSettings().then((rows) => {
const language = rows.find((row) => row.name === "language")?.value;
if (language) {
setField("i18n", language);
}
});
}, [isLoggedIn, setField]);
return null;
}
backend/functions/chatTranslation.js
const db = require("../models");
const LANGUAGE_SETTING_NAME = "language";
const DEFAULT_LANGUAGE = "en";
function normalizeLanguage(language) {
return String(language || DEFAULT_LANGUAGE)
.trim()
.toLowerCase()
.split("-")[0];
}
async function getUserLanguage(userId) {
if (!userId) {
return DEFAULT_LANGUAGE;
}
const row = await db.User_Settings.findOne({
where: { user_id: userId, name: LANGUAGE_SETTING_NAME, retired: false },
attributes: ["value"],
});
return normalizeLanguage(row?.value);
}
module.exports = {
DEFAULT_LANGUAGE,
normalizeLanguage,
getUserLanguage,
};
Translation-aware response serialization example:
const preferredLanguage = await getUserLanguage(req.user?.id);
const translatedMessageText =
record.translated_texts?.[preferredLanguage] || null;
return {
...record.toJSON(),
message_text:
translatedMessageText ||
record.original_message_text ||
record.message_text,
original_message_text: record.original_message_text,
translated_texts: record.translated_texts || {},
translated_to_language: translatedMessageText ? preferredLanguage : null,
has_translation: Boolean(translatedMessageText),
};
Recommended provider wrapper rule:
const TRANSLATION_PROVIDER_CANDIDATES = ["grok", "openai", "anthropic"];
Try providers in order and require at least one configured API key when dynamic translation is enabled.
Optional Redis
Only add this section after the user confirms they want Redis support.
What To Build
When Redis is requested, create:
config/redisConfig.jsfunctions/cacheUtils.jsmiddleware/cacheMiddleware.jsfunctions/modelCacheHooks.js— legacy: scaffold only if the source project still uses model-hook invalidation (verify per the premise check in Inspect First); otherwise invalidate explicitly in mutation paths
If the user also wants queued invalidation workers, create:
config/queueConfig.jsfunctions/queueWorkers.js
Redis Variables
REDIS_ENABLEDREDIS_HOSTREDIS_PORTREDIS_PASSWORDREDIS_DBREDIS_QUEUE_DBREDIS_CACHE_ENABLED
Recommended defaults:
REDIS_ENABLED=true
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_QUEUE_DB=1
REDIS_CACHE_ENABLED=true
Redis Implementation Steps
- Install
ioredis. - If queue workers are requested, install
bullmq. - Create
config/redisConfig.jswith singleton client creation, connection verification, graceful disable, and close handling. - Create
functions/cacheUtils.jswithget,set,del,invalidatePattern,invalidatePatterns,clearAll, andisAvailable. - Create
middleware/cacheMiddleware.jsthat caches successfulGETresponses and invalidates patterns after successful mutations. - Only if the source project still uses model-hook invalidation (check its current files first — some have retired it): create
functions/modelCacheHooks.jsthat attaches Sequelize lifecycle hooks for automatic invalidation. - If queue workers are enabled, push invalidation jobs to a
CACHE_INVALIDATIONqueue and process them asynchronously. - Initialize Redis during server startup, but degrade gracefully if Redis is unavailable.
Crucial Redis Code
config/redisConfig.js
const Redis = require("ioredis");
let redisClient = null;
let redisDisabled = process.env.REDIS_ENABLED === "false";
const createRedisClient = () => {
if (redisDisabled) return null;
if (redisClient) return redisClient;
redisClient = new Redis({
host: process.env.REDIS_HOST || "localhost",
port: parseInt(process.env.REDIS_PORT || "6379", 10),
password: process.env.REDIS_PASSWORD || undefined,
db: parseInt(process.env.REDIS_DB || "0", 10),
});
return redisClient;
};
const verifyRedisConnection = async () => {
const client = createRedisClient();
if (!client) return false;
try {
await client.ping();
return true;
} catch (error) {
redisDisabled = true;
return false;
}
};
const getRedisClient = () => (redisDisabled ? null : createRedisClient());
module.exports = { createRedisClient, getRedisClient, verifyRedisConnection };
functions/cacheUtils.js
const { getRedisClient } = require("../config/redisConfig");
class CacheManager {
constructor() {
this.defaultTTL = 300;
}
async get(key) {
const redis = getRedisClient();
if (!redis) return null;
const value = await redis.get(key);
return value ? JSON.parse(value) : null;
}
async set(key, value, ttl = this.defaultTTL) {
const redis = getRedisClient();
if (!redis) return false;
await redis.setex(key, ttl, JSON.stringify(value));
return true;
}
// scanStream, not redis.keys(): KEYS is O(N) and blocks Redis as key count grows
async invalidatePattern(pattern) {
const redis = getRedisClient();
if (!redis) return 0;
let removed = 0;
const stream = redis.scanStream({ match: pattern, count: 200 });
for await (const keys of stream) {
if (keys.length) {
await redis.del(...keys);
removed += keys.length;
}
}
return removed;
}
async invalidatePatterns(patterns = []) {
let removed = 0;
for (const pattern of patterns) {
removed += await this.invalidatePattern(pattern);
}
return removed;
}
isAvailable() {
return Boolean(getRedisClient());
}
}
module.exports = new CacheManager();
middleware/cacheMiddleware.js
const cacheManager = require("../functions/cacheUtils");
// Cache keys MUST include tenant scope AND the query string. A tenant-blind key
// serves company A's cached response to company B, and ignoring the query string
// returns cached page 1 for ?page=2.
const buildCacheKey = (req) => {
const companyId = req.user?.company_id ?? "public";
const locationId = req.user?.location_id ?? "-";
const query = new URLSearchParams(req.query).toString();
return [req.baseUrl, req.path, companyId, locationId, query].join(":");
};
const tenantScope = (req) =>
`${req.baseUrl}*:${req.user?.company_id ?? "public"}:*`;
const autoCacheMiddleware =
(ttl = 300) =>
async (req, res, next) => {
if (req.method !== "GET") {
const originalJson = res.json.bind(res);
res.json = (payload) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
const patterns = res.locals.cacheInvalidationPatterns || [
tenantScope(req),
];
cacheManager.invalidatePatterns(patterns).catch(() => {});
}
return originalJson(payload);
};
return next();
}
const cacheKey = buildCacheKey(req);
const cached = await cacheManager.get(cacheKey);
if (cached) {
return res.status(200).json(cached);
}
const originalJson = res.json.bind(res);
res.json = (payload) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
cacheManager.set(cacheKey, payload, ttl).catch(() => {});
}
return originalJson(payload);
};
next();
};
module.exports = { autoCacheMiddleware };
functions/modelCacheHooks.js (legacy — scaffold only if the source project still uses model-hook invalidation)
const cacheManager = require("./cacheUtils");
const setupAutoCacheInvalidation = (db) => {
Object.values(db).forEach((model) => {
if (!model?.addHook || !model?.rawAttributes) {
return;
}
const invalidate = async () => {
await cacheManager
.invalidatePattern(`*${model.tableName || model.name}*`)
.catch(() => {});
};
model.addHook("afterCreate", invalidate);
model.addHook("afterUpdate", invalidate);
model.addHook("afterDestroy", invalidate);
model.addHook("afterBulkCreate", invalidate);
model.addHook("afterBulkUpdate", invalidate);
model.addHook("afterBulkDestroy", invalidate);
});
};
module.exports = { setupAutoCacheInvalidation };
Server bootstrap snippet (the modelCacheHooks lines apply only if you scaffolded that legacy file — otherwise omit them and invalidate explicitly in mutation paths):
const {
createRedisClient,
verifyRedisConnection,
} = require("./config/redisConfig");
const { autoCacheMiddleware } = require("./middleware/cacheMiddleware");
const { setupAutoCacheInvalidation } = require("./functions/modelCacheHooks");
app.use("/api", autoCacheMiddleware());
createRedisClient();
const redisAvailable = await verifyRedisConnection();
if (redisAvailable) {
setupAutoCacheInvalidation(db);
}
Optional Live Data
Only add this section after the user confirms they want live data support.
The current implementation pattern uses the ws package. Do not silently replace it with Socket.IO.
What To Build
For backend-only live data, create:
functions/webSocketUtils.js- server bootstrap wiring for WebSocket upgrade handling and authentication
For full frontend live data, also create:
frontend/src/utils/wsManager.jsfrontend/src/contexts/WebSocketContext.jsx
Live Data Variables
NEXT_PUBLIC_WEBSOCKET— theNEXT_PUBLIC_prefix reaches the client bundle only in Next.js; on another frontend framework use its public-env convention (e.g.VITE_WEBSOCKET)AUTH0_DOMAIN/AUTH0_AUDIENCE— Auth0-specific; with a different JWT issuer, substitute its issuer/audience/JWKS config (thejwt.verifypattern below is unchanged)
The websocket shares the HTTP server's port via server.on("upgrade", ...) (the server snippet below) — there is no separate socket listener. Point NEXT_PUBLIC_WEBSOCKET at the backend HTTP port:
NEXT_PUBLIC_WEBSOCKET=ws://localhost:<backend HTTP port>
Add a SOCKET_PORT and a standalone listener only if the user explicitly asks for a separate socket server — never scaffold both patterns.
Live Data Implementation Steps
- Install
wson the backend. - Create
functions/webSocketUtils.jsexposingsetWebSocketServer,notifyClients,notifyUser, andnotifyUsers. - Create a
WebSocket.Serverwith{ noServer: true }in the backend server. - Attach
server.on("upgrade", ...)and forward upgrades to the websocket server. - Require clients to send an
authenticatemessage with a JWT. - Verify the JWT, load the user record, and store
socket.user,socket.company_id, andsocket.isAuthenticated. - Broadcast tenant-scoped updates using
company_idfiltering. - Send explicit
session-expiredorinvalid-tokenmessages before closing sockets on auth failure. - If frontend support is requested, create a websocket manager with capped reconnect backoff, a
disconnect()called in effect cleanup, and a provider that reconnects on login. - When live data is enabled, EVERY create/update/delete endpoint emits a tenant-filtered
notifyClientsevent as the default controller pattern — a scaffolded mutation with no broadcast is incomplete. - The frontend reconciles state from received events (patch the affected record/list in place) — never scaffold fetch-once views that require refresh.
- Verify with two clients: mutate from one, observe the other receive the event and update without refresh.
Crucial Live Data Code
functions/webSocketUtils.js
const WebSocket = require("ws");
let wss;
const setWebSocketServer = (server) => {
wss = server;
};
const notifyClients = async (messageType, data, filters = {}) => {
const payload = JSON.stringify({ type: messageType, data });
wss.clients.forEach((client) => {
if (client.readyState !== WebSocket.OPEN || !client.isAuthenticated) {
return;
}
if (
filters.company_id !== undefined &&
String(client.user.company_id) !== String(filters.company_id)
) {
return;
}
client.send(payload);
});
};
module.exports = { setWebSocketServer, notifyClients };
Backend server snippet (auth uses jwt.verify with JWKS — NEVER jwt.decode, which performs no signature check and lets any client forge any identity):
const WebSocket = require("ws");
const jwt = require("jsonwebtoken");
const jwksClient = require("jwks-rsa");
const { promisify } = require("util");
const { setWebSocketServer } = require("./functions/webSocketUtils");
const jwks = jwksClient({
jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
});
const getKey = (header, callback) => {
jwks.getSigningKey(header.kid, (err, key) => {
callback(err, key?.getPublicKey());
});
};
const verifyAsync = promisify(jwt.verify);
const wss = new WebSocket.Server({ noServer: true });
setWebSocketServer(wss);
wss.on("connection", (socket) => {
socket.isAuthenticated = false;
socket.on("message", async (rawMessage) => {
const message = JSON.parse(rawMessage);
if (message.type !== "authenticate") {
return;
}
let decoded;
try {
decoded = await verifyAsync(message.token, getKey, {
audience: process.env.AUTH0_AUDIENCE,
issuer: `https://${process.env.AUTH0_DOMAIN}/`,
algorithms: ["RS256"],
});
} catch (error) {
socket.send(JSON.stringify({ type: "invalid-token" }));
socket.close();
return;
}
const user = await db.User.findOne({ where: { auth_id: decoded.sub } });
if (!user) {
socket.send(JSON.stringify({ type: "invalid-token" }));
socket.close();
return;
}
socket.user = user.toJSON();
socket.company_id = user.company_id;
socket.isAuthenticated = true;
socket.send(
JSON.stringify({
type: "authenticated",
data: { id: socket.user.id, company_id: socket.company_id },
}),
);
});
});
server.on("upgrade", (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit("connection", ws, req);
});
});
Controller emission example:
const { notifyClients } = require("../functions/webSocketUtils");
// event naming: <model>-created|updated|deleted for YOUR models (e.g. order-updated)
await notifyClients("order-updated", order.toJSON(), {
company_id: order.company_id,
});
frontend/src/utils/wsManager.js
export class WebSocketManager {
constructor(url) {
this.url = url;
this.ws = null;
this.listeners = new Set();
this.baseDelay = 1000;
this.maxDelay = 30000;
this.attempts = 0;
this.closedByClient = false;
this.reconnectTimer = null;
}
connect(token) {
this.closedByClient = false;
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.attempts = 0;
this.ws.send(JSON.stringify({ type: "authenticate", token }));
};
this.ws.onmessage = ({ data }) => {
const message = JSON.parse(data);
this.listeners.forEach((listener) => listener(message));
};
// capped exponential backoff; never reconnect after a deliberate disconnect
this.ws.onclose = () => {
if (this.closedByClient) return;
const delay = Math.min(
this.baseDelay * 2 ** this.attempts,
this.maxDelay,
);
this.attempts += 1;
this.reconnectTimer = setTimeout(() => this.connect(token), delay);
};
}
disconnect() {
this.closedByClient = true;
clearTimeout(this.reconnectTimer);
this.ws?.close();
this.ws = null;
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
}
frontend/src/contexts/WebSocketContext.jsx
import { createContext, useContext, useEffect, useState } from "react";
import { wsManager } from "../utils/wsManager";
const WebSocketContext = createContext(null);
export const WebSocketProvider = ({ token, children }) => {
const [socket, setSocket] = useState(null);
useEffect(() => {
if (!token) {
return;
}
wsManager.connect(token);
const unsubscribe = wsManager.subscribe((message) => {
if (message.type === "authenticated") {
setSocket(wsManager.ws);
}
});
return () => {
unsubscribe();
wsManager.disconnect();
};
}, [token]);
return (
<WebSocketContext.Provider value={socket}>
{children}
</WebSocketContext.Provider>
);
};
export const useWebSocket = () => useContext(WebSocketContext);
Blueprint Example
Use this sequence when building a brand new project:
- Create the backend folder structure and
package.json. - Install
sequelize,sequelize-cli,pg, andpg-hstore. - Create
config/config.js,models/index.js, the initial Sequelize models and associations,functions/logger.js,config/dependencyOrder.js, andmigrations/. - Copy in
scripts/database-manager.jsandscripts/generate-migrations.js. - Add migration scripts and startup wiring, including
middleware/authMiddleware.js+middleware/permissionMiddleware.jsmounted before any route (see "Wire Startup In The Correct Order"). - Add
.envvalues for PostgreSQL and migration behavior. - Run
node scripts/database-manager.jswith safe diff settings enabled. - If the user wants Redis, add the Redis branch and verify graceful degradation.
- If the user wants live data, add the
wsbranch and verify authentication and company scoping. - If the user wants multi-language support, add the locale files, locale check scripts, provider wiring, user setting persistence, and translation-aware response flow.
- Scaffold the root
CLAUDE.mdproject contracts file and the rootplan.md(per project-plan-maintenance) with the new project's concrete names and the recorded decisions. - Run the End-of-Scaffold Boot Checklist. Only after it passes should you enable destructive flags such as dropping extra columns or tables.
Output Expectations
Good outcomes from this skill look like:
- a new project that accepts the copied migration files immediately: a PostgreSQL + Sequelize setup matching
database-managerexpectations, data access through Sequelize models and ORM patterns rather than hardcoded SQL, and a minimal logger + startup flow compatible with the copied scripts - optional Redis,
wslive data, and multi-language layers added ONLY when the user confirmed them — no parallel migration, cache, realtime, or i18n architecture introduced unnecessarily - short, concrete code snippets that remove ambiguity for the implementing agent
Anti-Patterns
Avoid these mistakes:
- rewriting
database-managerinstead of building the project around it - creating a different migration system alongside the copied one
- manually writing schema migrations when the manager should generate them
- embedding hardcoded SQL queries throughout controllers, services, or routes when Sequelize models would handle the job cleanly
- bypassing model associations and ORM methods for ordinary CRUD flows
- adding Redis, live data, or multi-language support without asking the user first (gates 8–10)
- teaching Socket.IO as if it were the current implementation
- replacing
react-intland user-language persistence with a different i18n approach without user direction - storing only translated text without preserving the original source text for dynamic content
- enabling destructive database flags before validation passes
- omitting
initializeBackendLogging()support when the scripts require it - forgetting company-scoped websocket filtering in a multi-tenant app
- any of the STOP-list red flags above (jwt.decode auth, tenant-blind cache keys, unbounded
findAll, float money / name-string joins) - shipping snippets whose imports don't resolve or whose invalidation is a console.log stub
- skipping the CLAUDE.md or plan.md contracts scaffold
Final Rule
Build the new project so the copied database-manager works first, keep the database backend centered on Sequelize instead of hardcoded queries, then add Redis, live data, and multilingual support only when the user explicitly wants those layers.