Six flows that replace surveillance with consent — exactly as implemented in the MiFamilias backend, mobile app, and admin portal. Every diagram below maps to real handlers, entities, and routes.
You've been posting about your dog. Want relevant offers — for how long?
One end-to-end pass through the system: a detected moment becomes a question, a question becomes a time-boxed window, and the window expires on its own.
flowchart LR
A(["📱 User posts about
their dog (3rd time
in 30 days)"]) --> B{{"🔔 Just-in-time prompt
"We noticed you're sharing
a lot about your pet""}}
B -->|"Yes — ☑ vets ☑ supplies
⏱ 1 month"| C["🟢 Interest windows open
AD_INTEREST#pets"]
B -->|"No thanks"| D["🚫 Dismissed
nothing stored,
nothing inferred"]
B -->|"Maybe later"| E["😴 Snoozed 7 days
then asked once more"]
C --> F["📺 Relevant ads served
labeled 'Because you asked'"]
F --> G{"User feedback
on every ad"}
G -->|"👍 Relevant"| F
G -->|"👎 Not relevant"| F
G -->|"🔕 Stop these"| H["⛔ Window ends early
ad hidden forever"]
C -.->|"⏰ time's up"| I["🌙 Auto-expires —
silence unless renewed"]
style A fill:#14262a,stroke:#34d399,color:#e8f4f0
style B fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0
style C fill:#0e3b2e,stroke:#34d399,color:#e8f4f0
style D fill:#14262a,stroke:#8aa6a0,color:#8aa6a0
style E fill:#14262a,stroke:#8aa6a0,color:#8aa6a0
style F fill:#14262a,stroke:#34d399,color:#e8f4f0
style G fill:#14262a,stroke:#34d399,color:#e8f4f0
style H fill:#2a1416,stroke:#f87171,color:#fecaca
style I fill:#14262a,stroke:#8aa6a0,color:#8aa6a0
Saying no is free: dismissed prompts store no preference, build no profile, and trigger a 60-day cooldown before that topic can ever be asked about again.
Hooked into createPost (fire-and-forget).
Five guards stand between a topic mention and a prompt.
flowchart TD
P["📝 New post text"] --> K{"Topic keywords?
pets · baby · wedding
home · travel · kids' sports"}
K -->|no| X1(["stop — nothing happens"])
K -->|yes| G1{"Ads enabled
for this user?"}
G1 -->|no| X2(["stop"])
G1 -->|yes| G2{"Guardian requires
approval for opt-ins?"}
G2 -->|"yes (protected minor)"| X3(["stop — never prompt"])
G2 -->|no| G3{"Pending or snoozed
prompt already open?"}
G3 -->|yes| X4(["stop — one question
at a time"])
G3 -->|no| G4{"Active window for
this category?"}
G4 -->|yes| X5(["stop — already opted in"])
G4 -->|no| G5{"Prompted about this
topic in last 60 days?"}
G5 -->|yes| X6(["stop — cooldown"])
G5 -->|no| CNT["AD_SIGNAL counter +1
(rolling 30-day window)"]
CNT --> TH{"3rd mention?"}
TH -->|no| W(["wait for more signal"])
TH -->|yes| PR["🔔 Create InterestPrompt
with plain-language reason
· counter discarded ·"]
style P fill:#14262a,stroke:#34d399,color:#e8f4f0
style PR fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0
style CNT fill:#14262a,stroke:#34d399,color:#e8f4f0
style K fill:#14262a,stroke:#34d399,color:#e8f4f0
style G1 fill:#14262a,stroke:#34d399,color:#e8f4f0
style G2 fill:#14262a,stroke:#34d399,color:#e8f4f0
style G3 fill:#14262a,stroke:#34d399,color:#e8f4f0
style G4 fill:#14262a,stroke:#34d399,color:#e8f4f0
style G5 fill:#14262a,stroke:#34d399,color:#e8f4f0
style TH fill:#14262a,stroke:#34d399,color:#e8f4f0
style X1 fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
style X2 fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
style X3 fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
style X4 fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
style X5 fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
style X6 fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
style W fill:#0e1b1e,stroke:#8aa6a0,color:#8aa6a0
Once a prompt is created the topic counter resets to zero — no profile accumulates beyond the rolling 30-day count. Service: interestSignalService.ts
An InterestWindow is keyed
USER#id / AD_INTEREST#category and
lazily expired at read time — no cron job needed.
stateDiagram-v2
direction LR
[*] --> Active : opt-in
(prompt · manual · guardian)
1w / 1mo / 3mo / 1yr
Active --> Active : 👍 relevant / 👎 not relevant
(feedback counters)
Active --> OptedOut : 🔕 "Stop these" on an ad
or End in Settings
Active --> Expired : ⏰ expiresAt passes
(lazy expiry at read)
Expired --> Active : 🔁 renew (new opt-in)
OptedOut --> Active : 🔁 user re-opts in
Expired --> [*]
OptedOut --> [*]
Routes: GET/POST /ads/interests · DELETE /ads/interests/:category — ended windows are kept for the user's own history, never for profiling.
The serving order inside GET /ads/active:
guardian restrictions first, then opt-outs, then the user's declared interests outrank everything.
sequenceDiagram
autonumber
participant App as 📱 Mobile App
participant API as ⚙️ getActiveAds
participant DB as 🗄️ DynamoDB
App->>API: GET /ads/active?placement=feed
API->>DB: subscription tier?
alt premium subscriber
API-->>App: no ads, ever 🏆
else free tier
API->>DB: active InterestWindows (lazy-expire)
API->>DB: AdPreferences (hidden ads, guardian rules)
API->>DB: active campaigns (GSI3 STATUS#active)
Note over API: 🛡️ drop guardian-restricted categories
(server-side, cannot be overridden)
Note over API: 🙈 drop ads the user opted out of
Note over API: ⭐ interest categories bypass targeting
scores AND client excludes — ranked first
API-->>App: ads — interest matches labeled
“Because you asked”
App->>API: POST /ads/:id/events (impression)
end
Smoke-verified: a user with no pets on their profile who opts into the pets category still receives pets ads — declared intent beats inferred targeting.
Three controls on every served ad feed three different systems — and feedback events cost the advertiser nothing.
flowchart LR
AD["📺 Served ad"] --> R["👍 Relevant"]
AD --> NR["👎 Not relevant"]
AD --> OO["🔕 Stop these"]
R --> C1["Ad relevantCount +1
Window relevantCount +1"]
NR --> C2["Ad notRelevantCount +1
Window notRelevantCount +1"]
OO --> C3["⛔ Window → opted_out
adId → hiddenAdIds
(never served again)"]
C1 --> IQ[("📊 IntentStats
STATS / INTENT")]
C2 --> IQ
C3 --> IQ
IQ --> DASH["Admin · Intent Quality tab
relevance scores per campaign"]
style AD fill:#14262a,stroke:#34d399,color:#e8f4f0
style R fill:#0e3b2e,stroke:#4ade80,color:#e8f4f0
style NR fill:#14262a,stroke:#fbbf24,color:#fde68a
style OO fill:#2a1416,stroke:#f87171,color:#fecaca
style C1 fill:#14262a,stroke:#34d399,color:#e8f4f0
style C2 fill:#14262a,stroke:#34d399,color:#e8f4f0
style C3 fill:#14262a,stroke:#f87171,color:#e8f4f0
style IQ fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0
style DASH fill:#14262a,stroke:#34d399,color:#e8f4f0
Extended event types on POST /ads/:adId/events: relevant · not_relevant · opt_out (alongside impression/click).
A verified guardian sets the rules for a minor once; the backend enforces them in every path — serving, prompting, and opting in.
flowchart TD
G["👨👧 Verified guardian"] -->|"PUT /ads/preferences/
guardian/:minorUserId"| RULES["🛡️ GuardianAdControls
✅ approved · ⛔ restricted
· require approval flag ·"]
RULES --> E1["Ad serving
restricted categories
never reach the minor"]
RULES --> E2["Signal detection
protected minors are
never prompted"]
RULES --> E3["Opt-in handlers
restricted category →
403 Forbidden"]
M["🧒 Minor tries to opt in
to a restricted category"] --> E3
E3 --> DENY(["⛔ Blocked server-side —
no client can override"])
style G fill:#14262a,stroke:#34d399,color:#e8f4f0
style RULES fill:#0e3b2e,stroke:#4ade80,stroke-width:2px,color:#e8f4f0
style E1 fill:#14262a,stroke:#34d399,color:#e8f4f0
style E2 fill:#14262a,stroke:#34d399,color:#e8f4f0
style E3 fill:#14262a,stroke:#34d399,color:#e8f4f0
style M fill:#14262a,stroke:#fbbf24,color:#fde68a
style DENY fill:#2a1416,stroke:#f87171,color:#fecaca
Guardianship is verified via family membership + adult check (verifyGuardianRelationship), the same trust machinery used by child-safety consent.