Build handoff · Fixera · Luna

One marketing flow, ten safe slices.

Preserve the working subscriber and Brevo foundation, then add dynamic languages, canonical services, exact shared audience resolution, test sends, lead imports, invitation campaigns, and cross-source deduplication in dependency order.

Safety gate Lead outreach remains disabled until legal/business approval is reflected in server configuration.

Decisions fixed before coding

Luna should not have to reinterpret the screenshots.

Compatibility

Keep the current campaign domain.

Retain newsletter, promotion, and reengagement; add invitation. Existing campaigns default to audienceType: subscribers and must continue to open, preview, schedule, send, retry, and refresh statistics.

Consent

Imported leads are not subscribers.

Leads remain a separate collection and can only be targeted by invitation campaigns while MARKETING_LEAD_OUTREACH_ENABLED=true. Any explicit unsubscribe suppresses the normalized email across subscribers and leads.

Language

Country suggests language; users choose it.

Use a server-owned catalog for EN/NL/FR/DE and country mappings. Explicit marketing-language preference wins, then stored locale, country default, and English fallback. Campaign content becomes a dynamic locale map.

Services

Select canonical values instead of typing strings.

Add a read-only service-options route protected by campaigns.manage. The searchable multi-select stores canonical service keys and temporarily reads legacy interestedServices.

Identity

One normalized email, at most one delivery.

Trim and lowercase only. Deduplicate before locale splitting and across sources/retries. Subscriber metadata wins over lead metadata; suppression wins over everything.

Scheduling

Display the truth about the existing scheduler.

Show scheduled plus local date/time while unsent. Preserve the current next-campaign-job dispatch semantics and say so in the UI; exact-minute scheduling is a separate infrastructure change.

Recommended architecture

A shared resolver is the load-bearing seam.

MarketingAudienceResolver
subscribersleadsboth

validate source/type → query sources → consent/suppression → filters → locale resolution → normalize/dedupe → exact counts → sendable recipients

Audience preview and production sending call this exact interface. The UI never calculates the authoritative set. The resolver returns source counts, deduplicated total, exclusions, fallback-language count, over-limit state, and a criteria hash.

Reuse

Keep working machinery

Subscriber sync, notification consent, signed unsubscribe route, Brevo blacklist reconciliation, atomic campaign claim/lease, per-locale lists, provider IDs, retries, footer attachment, 5,000 limit, and delivered-stat persistence.

Add

New focused modules

audienceResolver.ts, marketingCatalog.ts, normalizeEmail.ts, renderCampaign.ts, lead/import/suppression models, lead handler, spreadsheet parser, and focused frontend marketing components/pages.

Models and API surface

AreaRequired changeCompatibility rule
UsermarketingLocale + sourceExplicit choice beats country fallback.
SubscriberemailNormalized, first name, open locale string, serviceKeysRetain legacy fields during backfill.
LeadEmail identity, names, country, locale, services, import link, status, unsubscribe/delete timestampsSoft delete; separate from consented subscribers.
Lead importFilename/uploader/time/status/counts/capped row errorsNo workbook retention; commit is idempotent.
SuppressionUnique normalized email + reason/sourceBackfill existing explicit opt-outs.
CampaignAdd invitation, audience type, service keys, dynamic locale map, source/dedupe countsAll existing status/send fields remain.

Test send

Unsaved drafts must work

POST /api/admin/marketing-campaigns/test-send receives one email, locale, and current editor payload. It uses the production renderer through Brevo transactional email and never resolves an audience or changes campaign state/statistics.

Lead import

Validate, preview, then commit

Required columns: Email, Country, Language, Service. Optional: First name, Last name. In-memory workbook parsing, 5 MB/5,000-row caps, exact insert/update/duplicate/reject counts, repeat-safe commit, and durable metadata history.

Rendering

Greeting and footer are server contracts

Resolve/escape first name, prepend localized greeting, render body, append localized signed unsubscribe/preferences links. Inline and template-backed campaigns are validated against the same contract, and test sends use it too.

Dependency-ordered rollout

Ten slices Luna can build and verify independently.

01

Contracts and compatibility

Add invitation/audience types, catalog, normalization, resolved-recipient contracts, feature flag, and optional schema fields. Prove legacy campaigns default to subscribers.

02

User language and dynamic campaign content

Add user marketing locale, preference UI/API, open subscriber locale validation, dynamic content map, language endpoint, and locale backfill.

03

Canonical country and service controls

Add marketing service-options adapter, service-key compatibility/migration, and searchable country/service multi-selects with marketing-role authorization tests.

04

Shared subscriber resolver and exact preview

Move preview and send behind the shared resolver using subscribers first. Return exact counts, exclusions, fallback usage, limit state, and criteria hash.

05

Suppression and shared rendering

Backfill global suppression; extend unsubscribe; add localized greeting/footer renderer and template contract validation.

06

Test-send workflow

Add the unsaved-payload endpoint, Brevo transactional adapter, editor control, rate limiting, auditing, and no-mutation tests.

07

Lead models and import history

Add maintained spreadsheet parsing, validation preview, duplicate/update policy, idempotent commit, and import-history UI. Sending remains off.

08

Lead database UI and CRUD

Add search/filter/allowlisted sort/pagination, edit, soft delete, audit events, subscriber-match and suppression indicators.

09

Invitations and unified audiences

Add leads/both resolver branches, cross-source dedupe, legal flag enforcement, lead unsubscribe, source audit counts, and campaign audience selector.

10

Campaign-list completion

Wire q/status/type/audience filters, delivered count, scheduled local date/time, exact last preview, subscriber/lead navigation, and all loading/error/empty states.

Release gates

The work is done only when these flows pass.

FlowAcceptance
SubscribeUser enables Promotional email, chooses German, appears active, and can opt out from email or profile.
TargetingDE + Plumbing shows canonical services and German locale; exact preview matches country/service/locale/role filters.
Test sendUnsaved draft sends one [TEST] email with safe greeting and working links; campaign state and stats do not change.
Schedule/statsFuture unsent card shows scheduled local timestamp and documented next-job semantics; sent card shows delivered after refresh.
ImportWorkbook reports inserts, updates, duplicates, rejects before repeat-safe commit; history retains filename/time/counts.
Lead DBMarketing admin can search, filter every column, sort, edit, and soft-delete without losing import history.
DedupeBoth-source preview sends once per normalized email, assigns one locale, and uses subscriber metadata where available.
SuppressionAn explicit opt-out excludes the same email as subscriber and lead even when Brevo is temporarily unavailable.
RetryCompleted locale delivery is never recreated or resent after a partial failure/retry.
Legal gateLead preview/send is impossible while the feature flag or required legal configuration is absent.
Definition of done

Every original screenshot issue maps to a passing scenario; server tests and TypeScript build pass; frontend build/type checks pass; a Brevo dry-run proves preview/send parity; and a controlled internal live email proves greeting, localization, unsubscribe, deduplication, schedule display, and delivered-stat refresh.

Open the full Luna handoff with routes, model fields, migration order, critical files, and test matrix.

Source: read-only audit of the local Fixera frontend/backend on 21 August 2026. No Fixera source files were modified.