Build and maintain Niyati, a BFF-first astrology platform with mandatory TDD, immutable database patterns, and isolated infrastructure
AI-powered astrology platform following BFF architecture with JavaScript only. This skill guides development on Niyati, a lightweight chat-first personal astrology assistant.
**MANDATORY — No exceptions:**
1. **TDD/BDD is MANDATORY** — No code without tests first
2. **Database is IMMUTABLE** — No `UPDATE`, no `ALTER`. Always idempotent, from scratch
3. **Infrastructure is ISOLATED** — Dev, CI, and Prod use non-overlapping ports, networks, volumes
4. **Tests are INDEPENDENT** — Run via CI scripts, deploy scripts, or standalone. Always reproducible
```
Browser (React) → Caddy (proxy) → bff-platform/bff-auth → PostgreSQL
↓
n8n (AI agent) ← Ollama (LLM)
Worker ← Redis (queue) — handles async jobs
```
**MANDATORY for every code change:**
1. **Write test first** — Define expected behavior BEFORE implementation
2. **Run test (RED)** — Verify test fails for correct reason
3. **Implement minimal code (GREEN)** — Just enough to pass test
4. **Refactor** — Clean up while keeping tests green
5. **Run full suite** — Ensure no regressions
6. **Repeat** — Add next test case
**Backend testing:**
```bash
cd apps/bff-platform && npm test -- --watch queryClassifier.test.js
cd apps/bff-platform && npm test
cd apps/bff-auth && npm test
```
**E2E testing:**
```bash
cd e2e && npx playwright test
```
**Full CI verification:**
```bash
./scripts/ci-run-tests.sh
```
All BFF routes follow this structure:
```javascript
const { logger, sanitize, ErrorCodes } = require('@niyati/commons');
router.post('/action', async (req, res) => {
try {
const db = req.app.get('db');
const { input } = req.body;
if (!input) return res.sendError(ErrorCodes.VALIDATION_ERROR, 'Input required');
const result = await db.query('SELECT * FROM items WHERE id = $1', [sanitize(input)]);
if (result.rowCount === 0) return res.sendError(ErrorCodes.NOT_FOUND, 'Not found');
logger.info({ msg: 'action_success', id: input });
return res.sendSuccess({ data: result.rows[0] });
} catch (err) {
logger.error({ msg: 'action_failed', err: err.stack });
return res.sendError(ErrorCodes.INTERNAL_SERVER_ERROR, 'Failed');
}
});
```
Use test helpers from `packages/commons/test/helpers.js`:
```javascript
const request = require('supertest');
const { createTestApp, createMockDb, createMockCommons } = require('@test-helpers');
describe('My Feature', () => {
let app;
beforeEach(() => {
jest.resetModules();
jest.mock('@niyati/commons', () => createMockCommons());
const router = require('../lib/my-feature');
({ app } = createTestApp('/api/v1/my-feature', router));
});
afterEach(() => jest.restoreAllMocks());
test('POST /action returns success', async () => {
const mockDb = createMockDb({ rows: [{ id: 1 }], rowCount: 1 });
app.set('db', mockDb);
const res = await request(app)
.post('/api/v1/my-feature/action')
.send({ input: 'test' });
expect(res.statusCode).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.data).toHaveProperty('id', 1);
});
});
```
All hooks use `bffFetchWithRetry`:
```javascript
import { bffFetchWithRetry } from '../services/api';
export function useMyFeature() {
const performAction = async (payload) => {
const res = await bffFetchWithRetry('/api/v1/resource/action', {
method: 'POST',
body: JSON.stringify(payload)
});
return res.data;
};
return { performAction };
}
```
**Always idempotent, never ALTER:**
```sql
-- ✅ CORRECT: Idempotent table creation
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
phone_number VARCHAR(20) UNIQUE NOT NULL,
credits INTEGER DEFAULT 10
);
-- ✅ CORRECT: Idempotent index
CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone_number);
-- ✅ CORRECT: Idempotent seed data
INSERT INTO app_config (key, value)
VALUES ('credits_monthly_free', '10')
ON CONFLICT (key) DO NOTHING;
-- ❌ FORBIDDEN
ALTER TABLE users ADD COLUMN new_field VARCHAR(100);
UPDATE users SET credits = 0 WHERE expired = true;
```
**Database rebuild:**
```bash
docker compose down -v
docker compose up -d postgres
./scripts/db.sh migrate
./scripts/db.sh seed
```
UI is thin rendering layer only:
| ❌ Avoid in UI | ✅ Do in BFF |
|----------------|---------------|
| NLP/text classification | `/chat/classify` endpoint |
| Credit calculations | `/users/deduct-credits` |
| Date parsing/normalization | BFF normalizes to ISO |
| Complex validation | BFF validates and returns errors |
| Direct DB queries | All data through BFF |
**Pattern**: UI calls BFF → BFF processes → UI renders result
1. UI sends message to n8n webhook → receives AI response
2. UI calls `POST /api/v1/chat/classify` with `{message}` → returns `{queryType, creditCost, isBillable}`
3. If `isBillable`, UI calls `POST /api/v1/users/deduct-credits` with `{phoneNumber, amount}`
4. BFF deducts credits, returns updated balance
5. UI displays server-confirmed balance
**Query types** (in `apps/bff-platform/lib/queryClassifier.js`):
Always stub external services for determinism:
```javascript
const { test, expect } = require('@playwright/test');
test('user flow with stubbed API', async ({ page, baseURL }) => {
// Stub BFF responses
await page.route('**/api/v1/users/identify', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
status: 'ok',
data: { returning: true, user: { id: 1, credits: 10 } }
})
});
});
// Stub n8n webhook
await page.route('**/webhook/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({ output: "Today's horoscope..." })
});
});
await page.goto(baseURL + '/');
// ... interact and assert
});
```
Use `getSecret()` for Docker secrets:
```javascript
function getSecret(envVar, fileEnvVar) {
if (process.env[fileEnvVar]) {
return fs.readFileSync(process.env[fileEnvVar], 'utf8').trim();
}
return process.env[envVar];
}
const WORKER_TOKEN = getSecret('WORKER_TOKEN', 'WORKER_TOKEN_FILE');
```
1. **Never skip tests** — If asked to "just fix quickly", still write test first
2. **Never mutate database** — Use migrations with idempotent SQL only
3. **Never add heavy processing to UI** — Move to BFF if complex
4. **Always run `./scripts/ci-run-tests.sh`** before considering work complete
5. **Always use isolated infrastructure** — Different ports/volumes for dev/ci/prod
1. **Write test** → `apps/bff-platform/test/my-feature.test.js`
2. **Run test** → `npm test -- my-feature.test.js` (verify RED)
3. **Implement route** → `apps/bff-platform/lib/my-feature.js`
4. **Run test** → Verify GREEN
5. **Add E2E test** → `e2e/tests/my-feature.spec.js`
6. **Run full CI** → `./scripts/ci-run-tests.sh`
7. **Commit** → Only when all tests pass
Before considering any work complete:
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/niyati-astrology-platform-development/raw