Developer Docs

Privacy & Security APIs โ€” Developer Integration Guide

Overview

The Ngwenya platform provides a comprehensive privacy and security layer spanning two subgraphs:

  • nodes subgraph (NestJS/GraphQL): Cookie consent, GDPR data export, age verification
  • auth subgraph (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    โ”‚
        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜         โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

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 {
	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