Technical Requirements — Hrunit CRM
English technical requirements for the Next + Nest build. The requirements here
(data model, integrations, async model, security, audit, performance, testing, i18n) are
authoritative and carry over from the product's original spec; the stack mapping is
expressed for this project. Where a stack choice isn't final yet it's marked [open]
(see the decision list in the root CLAUDE.md).
Functional behaviour is described in
docs/how-it-works.htmlanddocs/product-requirements.md. On conflict, the functional docs win for what; this doc governs how.
1. Architecture
- Two apps in one monorepo:
apps/web(Next.js frontend) +apps/api(NestJS backend), shared TypeScript inpackages/. pnpm workspaces + Turborepo. - API-first. The Nest API is the single source of truth for all business operations; the web app consumes it. No business logic duplicated across the two.
- Business logic lives in services, never in controllers or React components. Controllers and components are thin — they call services. (Same rule the original enforced for its Actions/Services layer.)
- Single-tenant for now (one deployment per customer). Multi-tenancy is a later decision, not assumed in v1.
Layering (backend):
HTTP (Nest controllers, DTO validation)
→ Application (services / use-cases)
→ Domain (entities, value objects, enums)
→ Infrastructure (queue processors, integration clients, data layer)
→ Persistence (database, Redis)
2. Technology stack
| Concern | Choice | Status |
|---|---|---|
| Language | TypeScript (Node 22+) | fixed |
| Backend framework | NestJS | fixed |
| Frontend | Next.js (App Router) + React 19 + Tailwind 4 | fixed |
| UI primitives | shadcn/ui | fixed |
| Monorepo | pnpm workspaces + Turborepo | fixed |
| Database | PostgreSQL | [open] — recommended |
| ORM | Prisma (or Drizzle) | [open] — recommended |
| Cache / queue store | Redis | [open] — recommended |
| Background jobs | BullMQ | [open] — recommended |
| Realtime | Socket.IO (or a hosted service) | [open] |
| Auth | passwordless magic-link; session/JWT | [open] on token strategy |
| Authorization | role/permission layer (e.g. CASL) | [open] |
| Audit trail | append-only change log (library or in-house) | [open] |
| LLM | Anthropic Claude (pinned model per task) | fixed |
| Tests | Jest/Vitest (unit/integration) + Playwright (e2e) | [open] on runner |
| Lint / format | ESLint + Prettier | fixed |
| Commits / hooks | Conventional Commits (commitlint) + Husky + lint-staged | fixed |
3. Data model — core entities
Names are the ubiquitous language and must be kept:
User (1)─<(n) Role
User (1)─<(n) CampaignAccess >─(n) Campaign
Campaign (1)─<(n) Product
Campaign (1)─<(1) AssignmentRule
Campaign (1)─<(n) OutreachVariant / ConnectVariant / ResearchJob / Target
Target (1)─<(n) Contact, Department, ResearchDataPoint, OutreachMessage, Call, PipelineEvent
Target (1)─>(0..1) Campaign (may be "unassigned")
OutreachVariant (1)─<(n) OutreachStep ─<(n) StepVersion ─<(n) BanditEvent
ConnectVariant (1)─<(n) ConnectStep; (1)─<(n) Shift ─<(4) Package ─<(n) Target
VirtualCompany (1)─<(1..3) VirtualEmployee ─<(n) OutreachMessage
KnowledgeEntry (n)─<(n) OutreachStep/Variant
Special tables (requirements, not optional)
research_data_points— one row per fact, with provenance:target_id,attribute(canonical enum),value(JSON, shape-validated),source(<provider>[:step_model]ormanual:user_id:<id>),retrieved_at,confidence(0–1), optionalraw_response,api_call_id.research_api_calls— append-only audit of every external HTTP/LLM call: provider, endpoint,model_id, tokens,cost,duration_ms,status, error. Never UPDATE/DELETE.audit_logs— automatic change history (see §8).bandit_events— append-only:step_version_id,outcome(success/failure), timestamps, optionaltarget_id/outreach_message_id/call_id. Never UPDATE/DELETE.settings— key/value config; secrets stored encrypted (all third-party API creds).
Indexing (minimum)
targets(campaign_id, pipeline_status), targets(postal_code),
outreach_messages(target_id, sent_at), bandit_events(step_version_id, created_at),
audit_logs(auditable_type, auditable_id, created_at),
research_data_points(target_id, attribute) + (target_id, attribute, retrieved_at DESC),
research_api_calls(provider, requested_at) and (status, requested_at).
4. External integrations
| Service | Purpose | Cost | Call strategy |
|---|---|---|---|
| Anthropic Claude | LLM: mail generation, classification | per-token | sequential, retry, token budget |
| Serper.dev | Google Maps/Places | per-query | parallel (primary ID source) |
| Firecrawl.dev | website crawling | per-crawl | sequential, on demand |
| Wayback Machine | historical snapshots | free | parallel |
| Meta Ad Library | ad activity | free | parallel |
| jobsuche.api.bund.dev | job postings (primary) | free | parallel |
| SerpAPI Google Jobs | job postings (fallback) | per-query | only if bund.dev empty |
| TheirStack | hiring-manager ID | per-query | only if not found elsewhere |
| handelsregister.ai | company register | per-query | only if HR entry likely |
| INWX | domain purchases | API | on demand |
| Microsoft Graph | Exchange Online mailboxes | per-license | mail send/receive |
| Trulyinbox | email warmup | subscription | continuous background |
| CloudTalk | telephony + power-dialer | subscription | push + WebSocket (webhook fallback) |
| SignRequest | e-signatures | pay-as-you-go | on contract send |
| GoCardless | payment collection | per-transaction | after contract close |
Credentials live encrypted in settings (set via UI), never in env — except the
Anthropic key (infrastructure credential).
Every integration must: be its own Nest module behind an interface (for mocking); retry with exponential backoff; have a configurable timeout (default 30s); log requests/responses with secrets redacted; rate-limit where the provider requires; record cost per call; and be mocked in tests — never called for real in CI.
Failure handling: transient (5xx/timeout) → up to 3 retries with backoff; rate-limit (429) →
requeue after Retry-After; permanent (4xx) → fail that source, record in provenance, continue;
a source down > 15 min → escalation email to admin.
5. Async processing
Queues (BullMQ): default, research, outreach-mail, connect-sync, webhooks,
notifications. Sized for 30k targets / 20k mails per month; scale by adding workers.
Scheduled jobs: process research cron triggers (hourly); process follow-ups (daily); randomize sick days / plan vacations for virtual employees (daily); enforce campaign budgets (hourly); rotate unassigned targets (hourly); refresh stale research (daily).
Every job: has defined retry count + backoff; is idempotent where it matters (unique job key); logs start/end with an id; raises only on truly unexpected states (otherwise records an orderly failure).
Budget enforcement: before any paid call, pre-check against the campaign budget → abort if it would exceed; after the call, record actual cost; at 80% → warning email; at 100% → auto-pause the campaign + escalation email. This path must be 100% test-covered.
6. Realtime
WebSockets for: the live call cockpit (CloudTalk event → UI in < 500ms), pipeline updates
across users, and per-user notifications. Channels: per-user (user.{id}), per-shift (shift.{id}),
per-campaign (campaign.{id}.pipeline). Inbound CloudTalk arrives via WebSocket if offered, else via
signed webhooks that re-broadcast internally.
7. Security
- Auth: passwordless magic-link — tokens hashed at rest, single-use, 15-min TTL; invitation links have their own longer TTL. Rate-limit by IP + email; no user-enumeration (identical responses). API via tokens. 2FA infra optional.
- Authorization: roles Admin / Supervisor / Connect-Agent / Closing-Agent, per-resource checks. Connect-Agent sees only released campaigns; Closing-Agent only their assigned targets.
- User lifecycle: create=invite (passwordless); soft-delete invalidates sessions/tokens; the last active admin is protected from delete/deactivate/demote; email unique among live users only.
- Input validation: all input (HTTP, webhooks) validated via DTOs (
class-validator) /zod. Webhook payloads never trigger business logic unverified (verify signature first). - Secrets: none in code or
.env.example; third-party creds encrypted insettings; no real credentials in prompts or tests. - LLM safety: user/research content is XML-tag-isolated in prompts (prompt-injection defense); every LLM response is schema-validated before use; generated emails pass a plausibility check (required fields present, no hallucinated prices) before sending.
8. Provenance & audit
Every research data point carries source / retrieved_at / confidence (manual edits →
source = manual:user_id:<id>), surfaced in the UI on demand. An immutable audit trail records
who / old→new / IP / user-agent / tags on all business-relevant models (pipeline transitions,
campaign changes, step-version changes, permission changes, settings changes, manual corrections).
Audit records are not editable and not deletable. Retention: unlimited.
9. Internationalization
German (default) + English, per-user preference. All UI strings via the i18n layer (no hardcoded literals). Date/number formatting localized. Email templates and LLM prompts exist per language; system-generated messages follow the user's language. English in code, German in UI.
10. Testing
- Pyramid: unit (services, value objects, enums) · integration (full flows with faked external APIs) · e2e (login, pipeline transitions, live-call cockpit).
- Coverage: ≥ 70% overall; ≥ 90% in services / integrations; 100% on auth, permissions, and budget enforcement.
- External APIs are never called for real in tests/CI. Tests run offline and in parallel.
- Every bugfix ships with a regression test. Seed data / factories for local runs.
11. Performance targets
| Metric | Target |
|---|---|
| Web TTFB (normal pages) | < 300ms p95 |
| API response (CRUD) | < 200ms p95 |
| WebSocket event latency | < 500ms p95 |
| Research job per target | < 90s p95 |
| Mail send throughput | up to 1000 mails/hour peak |
| Pipeline list of 1000 targets | < 1s render |
12. Mobile
Mobile-first Tailwind; touch targets ≥ 44px; start-call / set-status are single-tap on mobile; the call cockpit must work on mobile; tables collapse to cards; native input types. No native app, no offline PWA in v1.
13. Reporting / BI (later)
Prepare for BI: a read-only DB user scoped to dedicated bi_* views; avoid soft-deletes on
reporting tables so figures stay consistent (documented exceptions only, e.g. users for
audit/forensics, with BI views filtering them out).
14. Out of scope (v1)
Reputation monitoring · GDPR/spam/telemarketing compliance layer · AI presentation generation · closing QA · in-instance multi-tenancy.