This document describes how Brain-Storm collects, stores, processes, and deletes personal data, and how the platform meets obligations under the General Data Protection Regulation (GDPR) and similar privacy regulations.
- Data Collection and Storage Practices
- User Data Export
- Data Deletion Procedures
- Cookie Policy and Consent Management
- Data Retention Policies
| Purpose | Legal basis (GDPR Art. 6) |
|---|---|
| Account creation and authentication | Contract performance (Art. 6(1)(b)) |
| Course progress and credential issuance | Contract performance (Art. 6(1)(b)) |
| KYC identity verification | Legal obligation (Art. 6(1)(c)) |
| Email notifications | Legitimate interest / consent (Art. 6(1)(a/f)) |
| Analytics and platform improvement | Legitimate interest (Art. 6(1)(f)) |
| Field | Description | Personal data |
|---|---|---|
id |
UUID primary key | No |
email |
Login identifier and contact address | Yes |
username |
Display name (optional) | Yes |
passwordHash |
bcrypt hash — plaintext never stored | No |
avatar |
Profile image URL | Yes |
bio |
Free-text self-description | Yes |
stellarPublicKey |
Stellar wallet address | Yes (pseudonymous) |
role |
student / instructor / admin |
No |
isBanned |
Account status flag | No |
isVerified |
Email verification status | No |
deletedAt |
Soft-delete timestamp | No |
verificationToken |
One-time email verification hash | No |
mfaEnabled |
MFA status flag | No |
mfaSecret |
TOTP secret (encrypted at rest) | Yes |
createdAt |
Account creation timestamp | No |
| Field | Description | Personal data |
|---|---|---|
stellarPublicKey |
Links KYC record to user | Yes (pseudonymous) |
status |
none / pending / approved / rejected |
No |
providerId |
Session ID from Synaps.io KYC provider | Yes |
KYC identity documents (passport, ID card, selfie) are processed and stored exclusively by the third-party provider Synaps.io. Brain-Storm does not receive or store raw identity documents.
Stores userId, notification type (enrollment, completion, credential_issued), and a message string. No additional personal data beyond the user reference.
Short-lived tokens linked to userId. Contain no personal data beyond the user reference and expiry timestamp.
Hashed API keys linked to userId. The plaintext key is shown once at creation and never stored.
Used for:
- Rate-limit counters (keyed by IP or user ID — no personal data stored)
- Short-lived cache entries (e.g. leaderboard, token balances — keyed by Stellar public key)
- Session data (if applicable)
Redis data is ephemeral and not persisted to disk in the default configuration.
Course completion credentials are recorded on the Stellar ledger as ManageData entries and via Soroban contract state. On-chain data is permanent and cannot be deleted. Only the Stellar public key (pseudonymous) and a course identifier are written on-chain — no names, emails, or other directly identifying information.
- Passwords are never stored in plaintext — only bcrypt hashes.
- MFA secrets are stored encrypted at rest.
username,avatar,bio, andstellarPublicKeyare all optional.- KYC documents are never stored by Brain-Storm — only the provider session ID and approval status.
| Processor | Purpose | Data shared |
|---|---|---|
| Synaps.io | KYC identity verification | Stellar public key, identity documents |
| Google OAuth | Social login | Email, name (from Google profile) |
| Sentry | Error monitoring | Stack traces, request metadata (no PII by default) |
| AWS (ECS, RDS, ElastiCache) | Infrastructure hosting | All data at rest |
Ensure Data Processing Agreements (DPAs) are in place with each processor before going to production.
GDPR Article 20 grants users the right to data portability — the right to receive their personal data in a structured, machine-readable format.
A dedicated data export endpoint does not yet exist. The following implementation is required to achieve GDPR compliance.
Add a GET /v1/users/:id/export endpoint that returns all personal data held for the authenticated user:
// users.controller.ts
@Get(':id/export')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Export all personal data for the authenticated user (GDPR Art. 20)' })
async exportData(
@Param('id') id: string,
@Request() req: { user: { id: string } },
) {
if (req.user.id !== id) throw new ForbiddenException();
return this.usersService.exportUserData(id);
}// users.service.ts
async exportUserData(id: string) {
const user = await this.repo.findOne({ where: { id } });
if (!user) throw new NotFoundException();
// Omit sensitive internal fields
const { passwordHash, verificationToken, mfaSecret, ...profile } = user;
return {
exportedAt: new Date().toISOString(),
profile,
// Extend to include enrollments, progress, notifications, credentials
};
}The response must be JSON (or optionally CSV) and must cover all tables that reference the user's ID.
A complete export must include:
| Data category | Source |
|---|---|
| Profile (email, username, avatar, bio, stellarPublicKey, role, createdAt) | users table |
| KYC status and provider reference | kyc_customers table |
| Course enrollments | enrollments table |
| Course progress | progress table |
| Notifications | notifications table |
| Credentials issued | Stellar ledger (transaction hashes) |
Users have the right to request deletion of their personal data. Brain-Storm implements soft deletion — setting deletedAt on the users row — which excludes the user from all queries while preserving referential integrity.
// UsersService.softDelete()
async softDelete(id: string) {
const user = await this.findById(id);
return this.repo.save({ ...user, deletedAt: new Date() });
}Soft-deleted users:
- Are excluded from all
findAllqueries (WHERE deletedAt IS NULL) - Cannot log in (authentication checks
deletedAt) - Retain their row for referential integrity with enrollments, credentials, and notifications
To fully honour erasure requests, a hard delete procedure must anonymise or remove the personal data fields while preserving non-personal records:
async hardDelete(id: string) {
await this.repo.update(id, {
email: `deleted-${id}@deleted.invalid`,
username: null,
avatar: null,
bio: null,
stellarPublicKey: null,
passwordHash: '',
mfaSecret: null,
verificationToken: null,
deletedAt: new Date(),
});
}Note: On-chain Stellar records (credentials,
ManageDataentries) are immutable and cannot be deleted. This is a known limitation of blockchain-based credential systems and should be disclosed in the platform's privacy notice. Only the Stellar public key (pseudonymous) is stored on-chain.
Admins can soft-delete any user via:
DELETE /v1/admin/users/:id
Authorization: Bearer <admin-jwt>Self-service deletion (user deleting their own account) requires a dedicated endpoint — currently not implemented.
KYC records in the kyc_customers table can be deleted directly. For data held by Synaps.io, submit a deletion request through their data subject request process per your DPA.
When a user is deleted, invalidate any cached entries keyed to their ID or Stellar public key:
await this.cacheManager.del(`token_balance:${user.stellarPublicKey}`);
await this.cacheManager.del('leaderboard:top50');Brain-Storm does not set first-party cookies for tracking or advertising. The following technical cookies may be set:
| Cookie | Purpose | Type | Duration |
|---|---|---|---|
access_token (if stored in cookie) |
JWT authentication | Strictly necessary | Session / JWT expiry |
next-intl locale preference |
Language selection | Functional | 1 year |
theme |
Dark/light mode preference | Functional | 1 year |
If JWTs are stored in
localStoragerather thanHttpOnlycookies, no authentication cookie is set. Confirm the storage mechanism before publishing the cookie policy.
- Sentry may set cookies for session replay if enabled. Review Sentry's cookie documentation and disable session replay if not required.
- Google OAuth sets cookies during the OAuth flow on
accounts.google.com— these are subject to Google's own cookie policy.
Under GDPR, strictly necessary cookies do not require consent. Functional and analytics cookies do.
A cookie consent banner must be added to the frontend before launch. Recommended approach:
- On first visit, display a consent banner before setting any non-essential cookies.
- Store the user's consent choice in
localStorage(e.g.cookie_consent: "accepted" | "rejected"). - Only initialise analytics/monitoring scripts after consent is granted.
// apps/frontend/src/lib/consent.ts
export function hasConsent(): boolean {
return localStorage.getItem('cookie_consent') === 'accepted';
}
export function setConsent(accepted: boolean) {
localStorage.setItem('cookie_consent', accepted ? 'accepted' : 'rejected');
}// Conditionally load Sentry only after consent
if (hasConsent()) {
await import('../instrument'); // Sentry initialisation
}| Data category | Retention period | Basis |
|---|---|---|
| Active user accounts | Duration of account + 30 days after deletion request | Contract |
| Soft-deleted user records | 30 days, then hard delete / anonymise | GDPR Art. 5(1)(e) |
| Password reset tokens | 1 hour (enforced at application level) | Minimisation |
| Email verification tokens | 24 hours (enforced at application level) | Minimisation |
| Refresh tokens | 7 days (or until revoked) | Security |
| API keys | Until revoked by user | Contract |
| Notifications | 90 days | Legitimate interest |
| KYC records (Brain-Storm) | Duration of account + 5 years | Legal obligation (AML) |
| KYC documents (Synaps.io) | Per Synaps.io DPA | Legal obligation |
| Application logs | 30 days | Legitimate interest |
| Redis cache entries | Per TTL (30 s – 5 min) | Technical necessity |
| On-chain Stellar records | Permanent (immutable) | Blockchain limitation |
Implement a scheduled job to enforce retention periods:
// Example: purge soft-deleted users older than 30 days
@Cron('0 2 * * *') // daily at 02:00
async purgeDeletedUsers() {
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
await this.repo
.createQueryBuilder()
.delete()
.from(User)
.where('deletedAt IS NOT NULL AND deletedAt < :cutoff', { cutoff })
.execute();
}| Right | GDPR Article | Current support | Action required |
|---|---|---|---|
| Right of access | Art. 15 | Partial (profile via GET /users/:id) |
Full export endpoint |
| Right to rectification | Art. 16 | ✅ PATCH /users/:id |
— |
| Right to erasure | Art. 17 | Partial (soft delete only) | Hard delete / anonymisation |
| Right to data portability | Art. 20 | ❌ Not implemented | Export endpoint |
| Right to restrict processing | Art. 18 | Partial (ban flag) | Formal restriction mechanism |
| Right to object | Art. 21 | ❌ Not implemented | Opt-out mechanism |
- security.md — Security scanning and vulnerability management
- stellar-integration.md — On-chain data and Stellar integration
- development-setup.md — Local environment setup