Imported from davsev/affheav (
AGENTS.md). Install upstream withnpx skills add davsev/affheav. Copyright stays with the author.
AGENTS.md
Agent and AI assistant instructions for the Affiliate Heaven project. This file defines conventions, constraints, and best practices that all agents must follow.
Project Overview
Affiliate Heaven automates affiliate product broadcasting to WhatsApp groups, Facebook pages, and Instagram accounts. The codebase is currently a Node.js/Express monolith being incrementally migrated to a microservices architecture using the Strangler Fig pattern — one service at a time, keeping the monolith running throughout.
Architecture & Migration Strategy
- Current state: Node.js/Express monolith with PostgreSQL and Google Sheets (legacy sync)
- Target state: Independent microservices — auth, user, products, ai-writer, channels, scheduler, broadcaster, frontend
- Migration approach: Strangler Fig — extract one service at a time behind the same
/api/v1/surface. Never break existing functionality while refactoring. - Service communication: BullMQ + Redis for async jobs; direct HTTP for synchronous calls between services
- New services use: TypeScript, Fastify or Hono, Drizzle ORM, Zod validation, Vitest
Commit & PR Standards
All commit messages and PR titles must follow Conventional Commits. This is enforced by CI — the "Lint PR title" check will fail and block the merge if the PR title does not conform.
Agent rule: Before opening or updating any PR, always rename the title to match
<type>(<scope>): <description>. Never leave a PR title as a plain sentence.
Format
<type>(<scope>): <description>
typeandscopeare lowercasedescriptionis lowercase, no trailing periodscopeis optional but strongly recommended — it locates the change in the codebase at a glance- Keep the description concise (≤ 72 characters total including type and scope)
Types
| Type | When to use |
|---|---|
feat |
New feature or user-visible capability |
fix |
Bug fix |
refactor |
Code change with no behavior change (e.g. i18n string extraction, restructuring) |
perf |
Performance improvement |
test |
Adding or updating tests only |
docs |
Documentation only (AGENTS.md, README, comments) |
chore |
Tooling, deps, config, scripts — nothing a user would notice |
ci |
CI/CD pipeline changes |
Scopes (use the closest match)
| Scope | Area |
|---|---|
auth |
Google OAuth, sessions, invite flow |
users |
User CRUD, roles, userService |
subjects |
Niche/subject management |
products |
Product table, sync, CRUD |
discovery |
Product suggestion / discovery agent |
scheduler |
Cron job manager |
workflow |
Broadcast pipeline (workflow.js) |
whatsapp |
WhatsApp / MacroDroid integration |
facebook |
Facebook Graph API integration |
instagram |
Instagram Content Publishing API |
openai |
Message generation, prompts |
aliexpress |
AliExpress API / scraping |
sheets |
Google Sheets sync (legacy) |
db |
Migrations, schema, DB utilities |
ui |
Frontend (public/app.js, index.html, modals, i18n strings) |
i18n |
Translation layer, locale strings, language switching |
logs |
Logging, SSE streaming |
config |
Environment, settings |
Examples
feat(discovery): use niche sales history to drive product suggestions
fix(scheduler): prevent duplicate cron jobs on hot reload
refactor(ui): replace hardcoded Hebrew strings with i18n t() calls
refactor(products): extract DB queries into repository layer
perf(db): add index on products.subject_id for faster joins
test(auth): add unit tests for invite token validation
chore(deps): upgrade express to 4.19.2
ci: add Vitest step to PR workflow
docs: update AGENTS.md with PR naming conventions
Common mistakes that fail CI
# ❌ Missing type prefix
Use niche sales history to drive product suggestions
# ❌ Type not lowercase
Feat(discovery): add niche suggestions
# ❌ Colon missing after scope
feat(discovery) add niche suggestions
# ❌ Trailing period
feat(discovery): add niche suggestions.
# ❌ Plain description with no conventional prefix (common agent mistake)
i18n: replace all hardcoded Hebrew strings in UI with t() calls
Security Review (Mandatory)
Every PR and every set of code changes must be reviewed with /security-review before merging.
This is non-negotiable. No exceptions for "small" or "infrastructure-only" changes — the hardcoded password incident in Phase 4 came from exactly that assumption.
Run it as the last step before opening or updating a PR:
/security-review
The review must pass (no high-severity findings) before the branch is merged to main.
Security Rules (Non-Negotiable)
Input Validation
- Every API endpoint must validate its input with a Zod schema before any business logic runs.
- Reject unknown fields — use
z.object({ ... }).strict()or strip extras explicitly. - Return
400with a structured error on validation failure — never let invalid data reach the DB.
// ✅ correct
const schema = z.object({ email: z.string().email(), subjectId: z.string().uuid() });
const parsed = schema.safeParse(req.body);
if (!parsed.success) return res.status(400).json({ success: false, errors: parsed.error.flatten() });
// ❌ wrong
const { email, subjectId } = req.body; // no validation
Credential Storage
- User API keys (Facebook tokens, AliExpress secrets, WhatsApp webhook URLs, Instagram tokens) are never stored as environment variables.
- All per-user credentials live in the
user_credentialstable, encrypted at rest with AES-256. - Only system-level values belong in env vars:
DATABASE_URL,GOOGLE_CLIENT_ID,SESSION_SECRET,ENCRYPTION_KEY, etc. - Credentials are never sent to the client. Return only boolean presence indicators:
{ hasFacebookToken: true }.
Authentication & Authorization
- All
/api/*routes require authentication. Never skip the auth middleware. - Role checks (
adminvsuser) must happen server-side on every request — never trust client-sent role claims. - Admin-only endpoints must check
req.user.role === 'admin'explicitly. - Use JWT (stateless) in new services; do not use session cookies in microservices.
SQL Injection Prevention
- Use parameterized queries exclusively:
query('SELECT * FROM users WHERE id = $1', [id]) - Never concatenate user input into SQL strings.
Sensitive Data in Logs
- Never log credentials, tokens, or personally identifiable information.
- Redact sensitive fields before logging:
{ ...subject, facebookToken: '[REDACTED]' }
Code Style & Quality
General
- Readability over cleverness. Code is written once, read many times. Prefer explicit over implicit.
- Abstract over specific. Write generic utilities; avoid hard-coding platform-specific logic in shared modules.
- Single responsibility. Each function does one thing. If a function needs a comment explaining what each section does, split it.
- No silent failures. Catch errors explicitly. Log and propagate; do not swallow errors silently in critical paths.
Naming
- Files:
kebab-casefor routes/scripts;camelCasefor services (userService.ts) - Functions/variables:
camelCase - Constants:
UPPER_SNAKE_CASE - Database columns:
snake_case - API response fields:
camelCase - Private/module-scoped state: prefix with
_
TypeScript (new services)
- Strict mode enabled:
"strict": trueintsconfig.json - No
any— useunknownand narrow explicitly - Define explicit return types on all exported functions
- Use
interfacefor shapes that can be extended;typefor unions and mapped types
Error Handling
- Use structured error responses:
{ success: false, error: string, errors?: object } - HTTP status codes:
400— validation error / bad request401— unauthenticated403— unauthorized (wrong role)404— resource not found409— conflict (duplicate)500— unexpected server error
- Service functions throw typed errors; route handlers catch and format them.
- External API calls (Facebook, WhatsApp, AliExpress, OpenAI) must be wrapped in a circuit breaker. A failure on one channel must not block others.
Imports
Order: Node core → third-party → local (separated by blank lines).
API Standards
- All routes versioned from day one:
/api/v1/... - Each service exposes an OpenAPI/Swagger spec (auto-generated, e.g., via
@fastify/swagger) - RESTful resource naming: plural nouns, lowercase, kebab-case:
/api/v1/user-credentials - Use HTTP methods semantically:
GETreads,POSTcreates,PUTreplaces,PATCHupdates partial,DELETEremoves - Pagination on list endpoints:
?page=1&limit=20; response includes{ data, total, page, limit }
Database
Schema Principles
- Normalized — no redundant columns
snake_casecolumn names- All tables have:
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),created_at TIMESTAMPTZ DEFAULT now(),updated_at TIMESTAMPTZ DEFAULT now() - Foreign keys must have explicit constraints (no orphan rows)
- Soft deletes where audit trail matters:
deleted_at TIMESTAMPTZ
Core Tables (target schema)
users — id, email, google_id, role, created_at
user_credentials — id, user_id, provider, key_name, encrypted_value, created_at
subjects — id, user_id, name, created_at (niches)
products — id, subject_id, user_id, url, image_url, text, sent_at, created_at
schedules — id, subject_id, user_id, cron_expression, enabled, created_at
broadcast_messages — id, subject_id, user_id, label, text, image_url, cron_expression, enabled, created_at
logs — id, user_id, subject_id, action, payload, created_at
invitations — id, email, token, invited_by, accepted_at, expires_at, created_at
Migrations
- Idempotent
CREATE TABLE IF NOT EXISTSfor schema bootstrapping - Migrations run on startup when
DATABASE_URLis set - Never mutate existing column types in-place in production without a migration plan
Testing
Agent rule: Every PR that adds or changes logic must include or update tests. A PR with no test changes for non-trivial logic changes will be rejected. If the change is purely cosmetic (CSS, string extraction, comment) tests are not required — say so explicitly in the PR body.
Requirements
- Unit tests are required for every new function, service method, and utility.
- Integration tests are required for any new API route or DB interaction.
- Test runner: Vitest (
npx vitest run) - Test files: co-located next to the source file —
userService.test.tsbesideuserService.ts - Test naming: describe what the function does, not how —
it('returns null when user is not found') - Mock external dependencies (DB, HTTP clients) — unit tests must not make real network calls
- Aim for edge cases: empty input, invalid types, missing optional fields, error paths
- Integration tests for service boundaries (DB interactions) use a test database
What counts as "logic"
Requires tests:
- New service methods or utilities
- New API routes
- Modified business rules (scheduling, delivery, permission checks)
- Bug fixes — add a regression test that would have caught the bug
Does not require tests (state this in the PR body):
- Pure string/i18n extraction (no logic change)
- CSS / layout changes
- Config or env var additions
- Documentation updates
Test structure
// Example structure
describe('findUser', () => {
it('returns the user when found by googleId', async () => { ... });
it('returns null when user does not exist', async () => { ... });
it('throws when DB query fails', async () => { ... });
});
CI
- GitHub Actions runs Vitest on every PR targeting
main - PRs cannot be merged if tests fail
- CI also runs TypeScript type-check (
tsc --noEmit) - A PR that skips tests without justification in the PR body will be flagged during review
External Integrations
- Wrap every external API call (Facebook Graph, WhatsApp/MacroDroid, Instagram, AliExpress, OpenAI, spoo.me) in a circuit breaker
- Never let one platform's failure propagate to others in the broadcast pipeline
- Retry transient errors (network timeouts) with exponential backoff — max 3 attempts
- Log every outbound API call with: provider, action, success/failure, duration
Environment Variables
Only system-level values belong in env vars. Per-user credentials go in the DB.
| Variable | Purpose |
|---|---|
DATABASE_URL |
PostgreSQL connection string |
ENCRYPTION_KEY |
AES-256 key for encrypting user credentials |
REDIS_URL |
BullMQ / queue connection |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
OAuth 2.0 (system-wide) |
GOOGLE_CALLBACK_URL |
OAuth redirect URI |
SESSION_SECRET |
Session signing (monolith only) |
JWT_SECRET |
JWT signing (new services) |
ADMIN_GOOGLE_EMAIL |
Bootstrap super-admin on first login |
APP_BASE_URL |
Invite link generation |
NODE_ENV |
development or production |
PORT |
Service port |
What Not To Do
- Do not store user API keys, tokens, or webhook URLs in
.envfiles - Do not send credentials or tokens to the client
- Do not skip input validation on any endpoint
- Do not use
anyin TypeScript - Do not write a function longer than ~60 lines without a strong reason — split it
- Do not commit secrets,
.envfiles, orconfig/google-credentials.json - Do not silently swallow errors in critical code paths
- Do not break the running monolith while extracting a service (Strangler Fig — keep both working)
- Do not add new env vars for per-user configuration — use
user_credentialstable instead