Privacy & Security APIs โ Developer Integration Guide
Overview
The Ngwenya platform provides a comprehensive privacy and security layer spanning two subgraphs:
nodessubgraph (NestJS/GraphQL): Cookie consent, GDPR data export, age verificationauthsubgraph (Rust/async-graphql): Session activity, security event log, account change hooks
These APIs fulfill GDPR Articles 15, 17, and 20, CCPA requirements, and general security best practices for protecting Visitor and Buyer data across the platform.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend (SvelteKit) โ
โ Cookie Banner ยท Settings ยท Security Dashboard โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโ
โ GraphQL Federation โ REST + GraphQL
โโโโโโโโผโโโโโโโ โโโโโโโโผโโโโโโโ
โ nodes โ โ auth โ
โ (NestJS) โ โ (Rust) โ
โ โ โ โ
โ โข Cookie โ hooks โ โข Sessions โ
โ โข GDPR โโโโโโโโโโบโ โข Audit Log โ
โ โข Age Gate โ โ โข Devices โ
โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
Cookie Consent API
Subgraph: nodes ยท Service: CookieConsentService ยท Resolver: CookieConsentResolver
GDPR and CCPA require explicit, granular consent for non-essential cookies. The platform stores consent preferences per-user with four categories:
| Category | Required? | Purpose |
|---|---|---|
necessary |
โ Always true | Session, CSRF, security |
analytics |
Optional | Usage tracking (e.g. Google Analytics) |
marketing |
Optional | Ad targeting, retargeting pixels |
functional |
Optional | Chat widgets, A/B testing, preferences |
IMPORTANT
The necessary category is always forced to true server-side, regardless of what the client sends. This invariant is enforced in CookieConsentService.updateConsent().
Query โ Get Current Consent
query {
cookieConsent {
necessary
analytics
marketing
functional
consentedAt
}
}
Returns defaults (all optional cookies false) if no consent has been recorded.
Mutation โ Granular Update
mutation {
updateCookieConsent(input: { analytics: true, marketing: false, functional: true }) {
necessary
analytics
marketing
functional
consentedAt
}
}
Only provided fields are updated โ omitted categories retain their current value.
Mutation โ Accept All / Reject All
# Cookie banner "Accept All" button
mutation {
acceptAllCookies {
analytics
marketing
functional
consentedAt
}
}
# Cookie banner "Reject All" button
mutation {
rejectAllCookies {
analytics
marketing
functional
consentedAt
}
}
Data Model
Cookie consent is stored as a subdocument on UserPreferences in MongoDB:
class CookieConsentPreferences {
necessary: boolean; // always true
analytics: boolean; // default: false
marketing: boolean; // default: false
functional: boolean; // default: false
consentedAt?: Date; // audit timestamp, set on every update
}
GDPR Data Export
Subgraph: nodes ยท Service: DataExportService ยท Resolver: DataExportResolver
Implements GDPR Article 20 โ Right to Data Portability. Aggregates all user-owned data from the nodes service into structured JSON.
Mutation โ Full Export
mutation {
requestDataExport {
userId
exportedAt
legalBasis
sections {
section
source
data
}
}
}
Response example:
{
"requestDataExport": {
"userId": "abc-123",
"exportedAt": "2026-04-05T22:00:00.000Z",
"legalBasis": "GDPR Article 20 โ Right to Data Portability",
"sections": [
{
"section": "Profile",
"source": "nodes",
"data": {
"displayName": "Jane",
"email": "jane@example.com",
"location": "Cape Town"
}
},
{
"section": "Preferences",
"source": "nodes",
"data": {
"theme": "dark",
"cookieConsent": { "analytics": true, "marketing": false },
"ageVerification": { "dateOfBirth": "2000-01-15" }
}
}
]
}
}
NOTE
Date of birth is intentionally included in the export โ it is the Visitor's own data per GDPR Article 15. The ageVerification section also includes verificationStatus and verificationMethod.
Query โ Preview Available Sections
query {
dataExportSections
}
# Returns: ["Profile", "Preferences", "Attributes", "Support Aliases"]
Exported Sections
| Section | Source | Contents |
|---|---|---|
| Profile | nodes |
displayName, bio, avatar, location, email, phone, timestamps |
| Preferences | nodes |
notifications, privacy, theme, cookie consent, age verification |
| Attributes | nodes |
All user attributes across all namespaces (key-value pairs) |
| Support Aliases | nodes |
Pseudo-anonymous identities used in Malet support interactions |
Architecture Note
The DataExportSection pattern is designed to be extensible. Future cross-service data (Murchase history, uCart contents, community activity) can be added via TCP MessagePattern calls to other subgraphs without modifying the core resolver:
interface DataExportSection {
section: string; // Human-readable label
source: string; // Subgraph that owns the data
data: Record<string, any> | Record<string, any>[];
}
Session Activity API
Subgraph: auth ยท Service: SessionService ยท Queries/Mutations: query.rs, mutation.rs
Exposes active sessions, devices, and IPs so Visitors can monitor and control where they are signed in.
Query โ List Active Sessions
query {
mySessions {
id
deviceName
deviceType
ipAddress
userAgent
isCurrent
createdAt
lastActivityAt
expiresAt
}
}
| Field | Type | Notes |
|---|---|---|
deviceName |
String | e.g. "Chrome on macOS" (parsed from user agent) |
deviceType |
String? | "pc", "smartphone", "tablet" |
ipAddress |
String? | Masked for privacy (e.g. 192.168.1.***.***) |
isCurrent |
Boolean | true if this is the session making the request |
lastActivityAt |
DateTime | Last request timestamp |
TIP
The isCurrent flag is determined by matching the session token from the request cookie against each session's stored token. This allows the frontend to highlight "This device" in the session list.
Mutation โ Revoke Single Session
mutation {
revokeSession(sessionId: "uuid-of-session-to-revoke")
}
Returns true on success. Cleans up both Redis (session + refresh tokens) and PostgreSQL. Logs a session_revoked audit event.
Mutation โ Sign Out Everywhere
mutation {
revokeAllSessions(keepCurrent: true) {
revokedCount
keptCurrent
}
}
| Parameter | Default | Description |
|---|---|---|
keepCurrent |
true |
If true, the calling session stays active |
Logs an all_sessions_revoked audit event (severity: critical).
Security Event Log
Subgraph: auth ยท Service: AuditService ยท Model: AuditEvent
A chronological audit trail of all security-relevant actions. Events are enriched with:
- Human-readable descriptions (e.g. "Failed sign-in attempt")
- Severity levels (
info,warning,critical) - IP masking for privacy
Query โ All Events
query {
mySecurityEvents(limit: 20) {
id
eventType
description
severity
authMethod
success
ipAddress
userAgent
errorMessage
createdAt
}
}
Query โ Filter by Event Type
query {
mySecurityEvents(limit: 10, eventType: "login_failed") {
description
severity
ipAddress
createdAt
}
}
Query โ Critical Events Only
query {
myCriticalSecurityEvents(limit: 20) {
eventType
description
createdAt
}
}
Returns only events classified as critical:
| Event Type | Description |
|---|---|
login_failed |
Failed sign-in attempt |
mfa_verify_failed |
Two-factor verification failed |
account_deactivated |
Account deactivated |
account_deleted |
Account permanently deleted |
all_sessions_revoked |
All sessions revoked (sign out everywhere) |
mfa_disabled |
Two-factor authentication disabled |
backup_code_used |
Backup code used for sign-in |
session_refresh_failed |
Failed to extend session |
Event Severity Classification
| Severity | Color | Event Types |
|---|---|---|
critical |
๐ด | Failed auth, account deletion, MFA disable, backup code usage |
warning |
๐ก | Email/phone/password changes, device revocation, session revocation, passkey registration |
info |
๐ต | Successful login, logout, session refresh, device trust changes |
Full Event Type Reference
The system tracks 24 distinct event types:
login_success ยท login_failed ยท logout
session_refresh ยท session_refresh_failed ยท session_revoked ยท all_sessions_revoked
device_trusted ยท device_untrusted ยท device_revoked ยท device_renamed
account_deactivated ยท account_reactivated ยท account_deleted
email_changed ยท phone_changed ยท password_changed
passkey_registered ยท passkey_prompt_dismissed
mfa_enrolled ยท mfa_disabled ยท mfa_verify_success ยท mfa_verify_failed
backup_code_used
Account Change Hooks
Sender: auth (Rust) ยท Receiver: nodes (NestJS)
When a Visitor changes their email or phone in the auth subgraph, a fire-and-forget webhook notifies the nodes subgraph to sync the profile data.
Architecture
โโโโโโโโโโโโโโโโโโ
โ auth (Rust) โ
โ contact.rs โ
โโโโโโโโโฌโโโโโโโโโ
โ POST /hooks/account-change
โ Authorization: Bearer <INTERNAL_WEBHOOK_SECRET>
โโโโโโโโโผโโโโโโโโโ
โ nodes (NestJS) โ
โ HookController โ
โโโโโโโโโโโโโโโโโโ
Webhook Payload
{
"userId": "uuid",
"field": "email",
"value": "new@example.com"
}
Valid field values: "email" or "phone".
Security
The webhook is authenticated with a shared secret (INTERNAL_WEBHOOK_SECRET). The nodes service validates this header before processing updates.
Environment Variables
| Variable | Service | Purpose |
|---|---|---|
INTERNAL_WEBHOOK_SECRET |
Both | Shared secret for webhook auth |
NODES_SERVICE_URL |
auth |
URL of the nodes service (e.g. http://nodes:3001) |
Age Verification
Subgraph: nodes ยท Model: AgeVerification on UserPreferences
Basic self-declared date-of-birth age gating for age-restricted Malet verticals (e.g. alcohol, tobacco).
Mutation โ Submit DOB
mutation {
submitDateOfBirth(dateOfBirth: "2000-01-15") {
verificationStatus
verificationMethod
verifiedAt
}
}
Query โ Check Eligibility
query {
checkAgeEligibility(minimumAge: 18)
}
# Returns: true or false
WARNING
This is a basic starting point โ self-declared DOB only. Production hardening is required before launching regulated Malet verticals. The roadmap includes ID verification providers, jurisdiction-specific rules, parental consent flows, and regulatory record-keeping. See libs/common/src/entities/age-verification.types.ts for the full enum scaffolding.
Content Visibility Matrix
Subgraph: nodes ยท Resolver: ContentVisibilityResolver ยท DTO: PrivacyPreferences
Controls how a Visitor's identity appears on community content โ reviews, blog comments, and "Helpful" votes. Users choose between three levels:
| Level | Display Name | Avatar | Badge |
|---|---|---|---|
FULL_NAME |
Full display name | Visible | ๐ข Public |
FIRST_NAME_ONLY |
"Jane D." format | Visible | ๐ก Partial |
ANONYMOUS |
"Anonymous User" | Hidden | ๐ฃ Anonymous |
IMPORTANT
Content visibility is a single global setting โ it applies uniformly to all community content types (reviews, comments, votes). There is no per-content-type granularity by design, to keep the privacy model simple and predictable.
Mutation โ Update Content Visibility
mutation {
updatePrivacySettings(input: { contentVisibility: ANONYMOUS }) {
id
contentVisibilityStatus
contentDisplayName
contentAvatarUrl
}
}
The mutation accepts any PrivacyPreferencesInput field, but for content visibility, only contentVisibility is relevant. The response includes the resolved display name and avatar, which the frontend uses for the live preview.
Query โ Get Current Setting
query {
me {
id
displayName
avatarUrl
contentVisibilityStatus
contentDisplayName
contentAvatarUrl
}
}
Resolved Fields
The ContentVisibilityResolver computes three read-only fields on the User type:
| Field | Type | Description |
|---|---|---|
contentDisplayName |
String! |
Computed name based on visibility setting |
contentAvatarUrl |
String |
Avatar URL, or null when anonymous |
contentVisibilityStatus |
ContentVisibility! |
Raw enum value for frontend badge rendering |
These fields are the canonical source of author identity across the federation. Other subgraphs (community, blogs) automatically receive these values when resolving a User reference.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Frontend (SvelteKit) โ
โ Settings โ Privacy โ ContentVisibilitySection โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ [Full Name] [First Name] [Anonymous] โ โ
โ โ โโโโโโโโโโ Live Preview โโโโโโโโโโ โ โ
โ โ ๐ค Jane D. ยท left a review ยท 2 days ago โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ updatePrivacySettings mutation
โโโโโโโโผโโโโโโโ
โ nodes โ
โ (NestJS) โ
โ โ
โ ContentVisibilityResolver โโโ computes display fields
โ UserMutationResolver โโโ updatePrivacySettings
โ PrivacyPreferences.contentVisibility (MongoDB)
โโโโโโโโโโโโโโโ
Frontend Integration
The feature is implemented in the Settings โ Privacy & Security section:
| File | Purpose |
|---|---|
components/settings/ContentVisibilitySection.svelte |
UI component with radio cards + live preview |
queries/auth.ts |
UPDATE_PRIVACY_SETTINGS + GET_CONTENT_VISIBILITY |
services/auth.ts |
updateContentVisibility() + getContentVisibility() |
components/settings/PrivacySecuritySection.svelte |
Parent component that renders the section |
Data Model
Content visibility is stored as a field on PrivacyPreferences in MongoDB:
class PrivacyPreferences {
profileVisibility: VisibilityPrivacy; // PUBLIC | PRIVATE
whoCanMessage: MessagePrivacy; // EVERYONE | FRIENDS | NOONE
whoCanConnect: ConnectionPrivacy; // EVERYONE | MUTUALS_ONLY
showFollowing: VisibilityPrivacy;
showFavorites: VisibilityPrivacy;
contentVisibility: ContentVisibility; // FULL_NAME | FIRST_NAME_ONLY | ANONYMOUS โ NEW
}
File Reference
nodes subgraph
| File | Purpose |
|---|---|
actors/user/cookie-consent.service.ts |
Cookie consent CRUD logic |
actors/user/cookie-consent.resolver.ts |
GraphQL mutations/queries for cookie consent |
actors/user/data-export.service.ts |
GDPR data aggregation across collections |
actors/user/data-export.resolver.ts |
GraphQL mutation/query for data portability |
actors/user/account-change-hook.controller.ts |
Webhook receiver for email/phone sync |
actors/user/dto/user-preferences.dto.ts |
Cookie consent + age verification + content visibility data models |
actors/user/content-visibility.resolver.ts |
Computes contentDisplayName, contentAvatarUrl, contentVisibilityStatus |
auth subgraph
| File | Purpose |
|---|---|
services/session.rs |
Session listing, revocation, sign-out-everywhere |
services/audit.rs |
Audit event logging, filtered queries |
models/audit.rs |
24 event types, severity levels, descriptions |
models/session.rs |
SessionActivityOutput with IP masking |
graphql/query.rs |
mySessions, mySecurityEvents, myCriticalSecurityEvents |
graphql/mutation.rs |
revokeSession, revokeAllSessions |
routes/graphql.rs |
SessionContext with token injection |
services/contact.rs |
Account change webhook sender |
Related
MFA & Passkeys โ TOTP, SMS 2FA, and WebAuthn passkey registration/authentication
Settings Architecture โ Frontend settings UI for privacy, security, and session management
User Listing API (Admin) โ Admin-only user management with privacy bypass
E2E Testing Infrastructure โ Auth mocking and cookie consent bypass patterns
IndexedDB Storage Layer โ Client-side data persistence and GDPR data export considerations
Wishlists & Collections โ Private client-side wishlist data architecture
uChat Client SDK โ E2EE messaging and content visibility integration
User Identity Resolution โ Shared
profileResolver.tsthat replaces raw UUIDs with handles or display names across all UI surfacesSupport Ticket Anonymity โ Pseudo-anonymous alias system protecting Visitor identity in Malet support interactions
uChat Online Presence โ Architecture and privacy considerations for real-time presence indicators.