Barazo AppView backend barazo.forum
at main 918 lines 42 kB view raw
1import { randomUUID } from 'node:crypto' 2import { eq, sql } from 'drizzle-orm' 3import { communitySettings } from '../db/schema/community-settings.js' 4import { communityOnboardingFields } from '../db/schema/onboarding-fields.js' 5import { users } from '../db/schema/users.js' 6import { pages } from '../db/schema/pages.js' 7import { categories } from '../db/schema/categories.js' 8import { topics } from '../db/schema/topics.js' 9import { replies } from '../db/schema/replies.js' 10import type { Database } from '../db/index.js' 11import { encrypt } from '../lib/encryption.js' 12import type { Logger } from '../lib/logger.js' 13import type { PlcDidService } from '../services/plc-did.js' 14 15// --------------------------------------------------------------------------- 16// Types 17// --------------------------------------------------------------------------- 18 19/** Result of getStatus(): either not initialized, or initialized with name. */ 20export type SetupStatus = { initialized: false } | { initialized: true; communityName: string } 21 22/** Parameters for community initialization. */ 23export interface InitializeParams { 24 /** Community DID (primary key for the settings row) */ 25 communityDid: string 26 /** DID of the authenticated user who becomes admin */ 27 did: string 28 /** Optional community name override */ 29 communityName?: string | undefined 30 /** Community handle (e.g. "community.barazo.forum"). Required for PLC DID generation. */ 31 handle?: string | undefined 32 /** Community service endpoint (e.g. "https://community.barazo.forum"). Required for PLC DID generation. */ 33 serviceEndpoint?: string | undefined 34} 35 36/** Result of initialize(): either success with details, or already initialized. */ 37export type InitializeResult = 38 | { 39 initialized: true 40 adminDid: string 41 communityName: string 42 communityDid?: string | undefined 43 } 44 | { alreadyInitialized: true } 45 46/** Setup service interface for dependency injection and testing. */ 47export interface SetupService { 48 getStatus(communityDid: string): Promise<SetupStatus> 49 initialize(params: InitializeParams): Promise<InitializeResult> 50} 51 52// --------------------------------------------------------------------------- 53// Constants 54// --------------------------------------------------------------------------- 55 56const DEFAULT_COMMUNITY_NAME = 'Barazo Community' 57 58// --------------------------------------------------------------------------- 59// Factory 60// --------------------------------------------------------------------------- 61 62/** 63 * Create a setup service for managing community initialization. 64 * 65 * The first authenticated user to call initialize() becomes the community admin. 66 * When handle and serviceEndpoint are provided, a PLC DID is generated and 67 * registered with plc.directory. 68 * 69 * @param db - Drizzle database instance 70 * @param logger - Pino logger instance 71 * @param encryptionKey - KEK for encrypting sensitive data (AI_ENCRYPTION_KEY) 72 * @param plcDidService - Optional PLC DID service for DID generation 73 * @returns SetupService with getStatus and initialize methods 74 */ 75export function createSetupService( 76 db: Database, 77 logger: Logger, 78 encryptionKey: string, 79 plcDidService?: PlcDidService 80): SetupService { 81 /** 82 * Check whether the community has been initialized. 83 * 84 * @param communityDid - The community DID to check status for 85 * @returns SetupStatus indicating initialization state 86 */ 87 async function getStatus(communityDid: string): Promise<SetupStatus> { 88 try { 89 const rows = await db 90 .select({ 91 initialized: communitySettings.initialized, 92 communityName: communitySettings.communityName, 93 }) 94 .from(communitySettings) 95 .where(eq(communitySettings.communityDid, communityDid)) 96 97 const row = rows[0] 98 99 if (!row || !row.initialized) { 100 return { initialized: false } 101 } 102 103 return { initialized: true, communityName: row.communityName } 104 } catch (err: unknown) { 105 logger.error({ err }, 'Failed to get setup status') 106 throw err 107 } 108 } 109 110 /** 111 * Initialize the community with the first admin user. 112 * 113 * Uses an atomic upsert to prevent race conditions: INSERT new row, or 114 * UPDATE existing if not yet initialized. The WHERE clause ensures an 115 * already-initialized row is never overwritten. 116 * 117 * If handle and serviceEndpoint are provided and a PlcDidService is 118 * available, generates a PLC DID with signing + rotation keys and 119 * registers it with plc.directory. 120 * 121 * @param params - Initialization parameters 122 * @returns InitializeResult with the new state or conflict indicator 123 */ 124 async function initialize(params: InitializeParams): Promise<InitializeResult> { 125 const { communityDid, did, communityName, handle, serviceEndpoint } = params 126 127 try { 128 // Generate PLC DID if handle and serviceEndpoint are provided 129 let plcDid: string | undefined 130 let signingKeyHex: string | undefined 131 let rotationKeyHex: string | undefined 132 133 if (handle && serviceEndpoint && plcDidService) { 134 logger.info({ handle, serviceEndpoint }, 'Generating PLC DID during community setup') 135 136 const didResult = await plcDidService.generateDid({ 137 handle, 138 serviceEndpoint, 139 }) 140 141 plcDid = didResult.did 142 signingKeyHex = encrypt(didResult.signingKey, encryptionKey) 143 rotationKeyHex = encrypt(didResult.rotationKey, encryptionKey) 144 145 logger.info({ plcDid, handle }, 'PLC DID generated successfully') 146 } else if (handle && serviceEndpoint && !plcDidService) { 147 logger.warn( 148 { handle, serviceEndpoint }, 149 'PLC DID generation requested but PlcDidService not available' 150 ) 151 } 152 153 // Atomic upsert: INSERT new row, or UPDATE existing if not yet initialized. 154 // The WHERE clause ensures an already-initialized row is never overwritten. 155 const rows = await db 156 .insert(communitySettings) 157 .values({ 158 communityDid, 159 initialized: true, 160 adminDid: did, 161 communityName: communityName ?? DEFAULT_COMMUNITY_NAME, 162 handle: handle ?? null, 163 serviceEndpoint: serviceEndpoint ?? null, 164 signingKey: signingKeyHex ?? null, 165 rotationKey: rotationKeyHex ?? null, 166 }) 167 .onConflictDoUpdate({ 168 target: communitySettings.communityDid, 169 set: { 170 initialized: true, 171 adminDid: did, 172 communityName: communityName ? communityName : sql`${communitySettings.communityName}`, 173 handle: handle ?? sql`${communitySettings.handle}`, 174 serviceEndpoint: serviceEndpoint ?? sql`${communitySettings.serviceEndpoint}`, 175 signingKey: signingKeyHex ?? sql`${communitySettings.signingKey}`, 176 rotationKey: rotationKeyHex ?? sql`${communitySettings.rotationKey}`, 177 updatedAt: new Date(), 178 }, 179 where: eq(communitySettings.initialized, false), 180 }) 181 .returning({ 182 communityName: communitySettings.communityName, 183 communityDid: communitySettings.communityDid, 184 }) 185 186 const row = rows[0] 187 if (!row) { 188 logger.warn({ did }, 'Setup initialize attempted on already-initialized community') 189 return { alreadyInitialized: true } 190 } 191 192 // Promote the initializing user to admin in the users table 193 await db.update(users).set({ role: 'admin' }).where(eq(users.did, did)) 194 logger.info({ did }, 'User promoted to admin role') 195 196 // Seed platform onboarding fields 197 await db 198 .insert(communityOnboardingFields) 199 .values({ 200 id: 'platform:age_confirmation', 201 communityDid, 202 fieldType: 'age_confirmation', 203 label: 'Age Declaration', 204 description: 205 'Please select your age bracket. This determines which content is available to you.', 206 isMandatory: true, 207 sortOrder: -1, 208 source: 'platform', 209 config: null, 210 }) 211 .onConflictDoNothing() 212 logger.info({ communityDid }, 'Platform onboarding fields seeded') 213 214 // Seed default pages (Terms of Service, Privacy Policy, Cookie Policy) 215 const now = new Date() 216 const pageDefaults = [ 217 { 218 id: `page-${randomUUID()}`, 219 slug: 'terms-of-service', 220 title: 'Terms of service', 221 content: TERMS_OF_SERVICE_CONTENT, 222 status: 'published' as const, 223 metaDescription: 'Terms and conditions for using this forum community.', 224 parentId: null, 225 sortOrder: 0, 226 communityDid, 227 createdAt: now, 228 updatedAt: now, 229 }, 230 { 231 id: `page-${randomUUID()}`, 232 slug: 'privacy-policy', 233 title: 'Privacy policy', 234 content: PRIVACY_POLICY_CONTENT, 235 status: 'published' as const, 236 metaDescription: 'How we collect, use, and protect your personal data.', 237 parentId: null, 238 sortOrder: 1, 239 communityDid, 240 createdAt: now, 241 updatedAt: now, 242 }, 243 { 244 id: `page-${randomUUID()}`, 245 slug: 'cookie-policy', 246 title: 'Cookie policy', 247 content: COOKIE_POLICY_CONTENT, 248 status: 'published' as const, 249 metaDescription: 'Information about the cookies used on this forum.', 250 parentId: null, 251 sortOrder: 2, 252 communityDid, 253 createdAt: now, 254 updatedAt: now, 255 }, 256 { 257 id: `page-${randomUUID()}`, 258 slug: 'accessibility', 259 title: 'Accessibility statement', 260 content: ACCESSIBILITY_CONTENT, 261 status: 'published' as const, 262 metaDescription: 263 'Barazo is committed to WCAG 2.2 Level AA accessibility. Learn about our testing, standards, and how to report issues.', 264 parentId: null, 265 sortOrder: 3, 266 communityDid, 267 createdAt: now, 268 updatedAt: now, 269 }, 270 { 271 id: `page-${randomUUID()}`, 272 slug: 'your-data', 273 title: 'Your data', 274 content: YOUR_DATA_CONTENT, 275 status: 'published' as const, 276 metaDescription: 277 'How your data works on this forum. Built on the AT Protocol, your posts are public by default and your identity is portable.', 278 parentId: null, 279 sortOrder: 4, 280 communityDid, 281 createdAt: now, 282 updatedAt: now, 283 }, 284 ] 285 await db.insert(pages).values(pageDefaults) 286 logger.info({ communityDid, pageCount: pageDefaults.length }, 'Default pages seeded') 287 288 // Seed default categories with subcategories 289 const catGeneral = `cat-${randomUUID()}` 290 const catDev = `cat-${randomUUID()}` 291 const catDevFrontend = `cat-${randomUUID()}` 292 const catDevBackend = `cat-${randomUUID()}` 293 const catDevDevops = `cat-${randomUUID()}` 294 const catCommunity = `cat-${randomUUID()}` 295 const catCommunityShowcase = `cat-${randomUUID()}` 296 const catCommunityEvents = `cat-${randomUUID()}` 297 const catFeedback = `cat-${randomUUID()}` 298 const catFeedbackBugs = `cat-${randomUUID()}` 299 const catFeedbackFeatures = `cat-${randomUUID()}` 300 301 const categoryDefaults = [ 302 // Root categories 303 { 304 id: catGeneral, 305 slug: 'general', 306 name: 'General', 307 description: 'Open discussion on any topic.', 308 parentId: null, 309 sortOrder: 0, 310 communityDid, 311 maturityRating: 'safe' as const, 312 createdAt: now, 313 updatedAt: now, 314 }, 315 { 316 id: catDev, 317 slug: 'development', 318 name: 'Development', 319 description: 'Technical discussions about software development.', 320 parentId: null, 321 sortOrder: 1, 322 communityDid, 323 maturityRating: 'safe' as const, 324 createdAt: now, 325 updatedAt: now, 326 }, 327 { 328 id: catCommunity, 329 slug: 'community', 330 name: 'Community', 331 description: 'Community news, events, and member introductions.', 332 parentId: null, 333 sortOrder: 2, 334 communityDid, 335 maturityRating: 'safe' as const, 336 createdAt: now, 337 updatedAt: now, 338 }, 339 { 340 id: catFeedback, 341 slug: 'feedback', 342 name: 'Feedback', 343 description: 'Help us improve — report bugs and suggest features.', 344 parentId: null, 345 sortOrder: 3, 346 communityDid, 347 maturityRating: 'safe' as const, 348 createdAt: now, 349 updatedAt: now, 350 }, 351 // Subcategories: Development 352 { 353 id: catDevFrontend, 354 slug: 'frontend', 355 name: 'Frontend', 356 description: 'UI frameworks, CSS, accessibility, and browser APIs.', 357 parentId: catDev, 358 sortOrder: 0, 359 communityDid, 360 maturityRating: 'safe' as const, 361 createdAt: now, 362 updatedAt: now, 363 }, 364 { 365 id: catDevBackend, 366 slug: 'backend', 367 name: 'Backend', 368 description: 'Servers, databases, APIs, and system design.', 369 parentId: catDev, 370 sortOrder: 1, 371 communityDid, 372 maturityRating: 'safe' as const, 373 createdAt: now, 374 updatedAt: now, 375 }, 376 { 377 id: catDevDevops, 378 slug: 'devops', 379 name: 'DevOps', 380 description: 'CI/CD, containers, infrastructure, and deployment.', 381 parentId: catDev, 382 sortOrder: 2, 383 communityDid, 384 maturityRating: 'safe' as const, 385 createdAt: now, 386 updatedAt: now, 387 }, 388 // Subcategories: Community 389 { 390 id: catCommunityShowcase, 391 slug: 'showcase', 392 name: 'Showcase', 393 description: 'Share what you have built with the community.', 394 parentId: catCommunity, 395 sortOrder: 0, 396 communityDid, 397 maturityRating: 'safe' as const, 398 createdAt: now, 399 updatedAt: now, 400 }, 401 { 402 id: catCommunityEvents, 403 slug: 'events', 404 name: 'Events', 405 description: 'Meetups, conferences, and community happenings.', 406 parentId: catCommunity, 407 sortOrder: 1, 408 communityDid, 409 maturityRating: 'safe' as const, 410 createdAt: now, 411 updatedAt: now, 412 }, 413 // Subcategories: Feedback 414 { 415 id: catFeedbackBugs, 416 slug: 'bugs', 417 name: 'Bug Reports', 418 description: 'Report issues so we can fix them.', 419 parentId: catFeedback, 420 sortOrder: 0, 421 communityDid, 422 maturityRating: 'safe' as const, 423 createdAt: now, 424 updatedAt: now, 425 }, 426 { 427 id: catFeedbackFeatures, 428 slug: 'feature-requests', 429 name: 'Feature Requests', 430 description: 'Suggest new features or improvements.', 431 parentId: catFeedback, 432 sortOrder: 1, 433 communityDid, 434 maturityRating: 'safe' as const, 435 createdAt: now, 436 updatedAt: now, 437 }, 438 ] 439 440 await db.insert(categories).values(categoryDefaults) 441 logger.info( 442 { communityDid, categoryCount: categoryDefaults.length }, 443 'Default categories seeded' 444 ) 445 446 // Seed demo topics and replies so the forum feels alive on first visit. 447 // Uses the admin's DID as author. URIs use a synthetic namespace to avoid 448 // collisions with real AT Protocol records from the firehose. 449 const demoTopics = [ 450 { 451 category: 'general', 452 title: 'Welcome to the community!', 453 content: 454 'This is a brand new forum powered by the AT Protocol. Your identity is portable, your data is yours, and the community is decentralized.\n\nFeel free to introduce yourself and start a conversation.', 455 tags: ['welcome', 'introduction'], 456 replyContent: 457 'Excited to be here! The AT Protocol integration is a great touch — portable identity is the future.', 458 }, 459 { 460 category: 'frontend', 461 title: 'What frontend framework are you using?', 462 content: 463 'Curious what everyone is building with these days. React? Vue? Svelte? Something else entirely?\n\nBonus points if you can explain *why* you chose it over the alternatives.', 464 tags: ['frontend', 'frameworks', 'discussion'], 465 replyContent: 466 'SolidJS for new projects, React for anything with a large ecosystem requirement. The signals model in Solid feels like the future of reactivity.', 467 }, 468 { 469 category: 'backend', 470 title: 'Database migration strategies for zero-downtime deploys', 471 content: 472 'We have been running into issues with schema migrations that lock tables during deployment. Has anyone implemented a reliable expand-and-contract pattern?\n\nLooking for practical advice, not just theory.', 473 tags: ['database', 'migrations', 'deployment'], 474 replyContent: 475 'The expand-contract pattern works well. Key insight: never rename columns in a single migration. Add the new column, backfill, switch reads, then drop the old one.', 476 }, 477 { 478 category: 'devops', 479 title: 'Docker Compose vs Kubernetes for small teams', 480 content: 481 'Our team of 4 is debating whether to move from Docker Compose to Kubernetes. Current setup handles ~10k requests/day on a single VPS.\n\nIs K8s overkill at this scale? What would make you switch?', 482 tags: ['docker', 'kubernetes', 'infrastructure'], 483 replyContent: 484 'At 10k req/day, Compose is perfectly fine. We made the switch at ~500k req/day when we needed auto-scaling and rolling deploys across multiple nodes.', 485 }, 486 { 487 category: 'showcase', 488 title: 'Built a real-time markdown editor with AT Protocol sync', 489 content: 490 'Just finished a side project: a markdown editor that syncs documents to your PDS as AT Protocol records. Edits propagate in real-time via the firehose.\n\nSource is on GitHub — feedback welcome!', 491 tags: ['atproto', 'project', 'open-source'], 492 replyContent: 493 'This is impressive. How do you handle conflict resolution when two clients edit the same document simultaneously?', 494 }, 495 { 496 category: 'bugs', 497 title: '[Example] How to write a good bug report', 498 content: 499 'A good bug report includes:\n\n1. **What you expected** to happen\n2. **What actually happened** (screenshots help!)\n3. **Steps to reproduce** the issue\n4. **Environment details** — browser, OS, screen size\n\nThe more detail you provide, the faster we can fix it.', 500 tags: ['meta', 'guide'], 501 replyContent: 502 'Adding browser console output (F12 → Console tab) is also incredibly helpful for tracking down frontend issues.', 503 }, 504 { 505 category: 'feature-requests', 506 title: '[Example] Dark mode toggle in user preferences', 507 content: 508 'It would be great to have a dark mode option in user settings. Currently the theme follows the system preference, but I would like to override it per-forum.\n\n**Use case:** I prefer dark mode at night but light mode during the day, and my system setting does not auto-switch.', 509 tags: ['ux', 'accessibility', 'theming'], 510 replyContent: 511 'Strong support for this. A three-way toggle (Light / Dark / System) is the standard pattern. Could even store the preference in the PDS for cross-forum portability.', 512 }, 513 ] 514 515 const topicValues = demoTopics.map((t, i) => { 516 const rkey = `seed${String(i + 1).padStart(3, '0')}` 517 return { 518 uri: `at://${did}/forum.barazo.topic.post/${rkey}`, 519 rkey, 520 authorDid: did, 521 title: t.title, 522 content: t.content, 523 category: t.category, 524 tags: t.tags, 525 communityDid, 526 cid: `bafyreiseed${String(i + 1).padStart(3, '0')}`, 527 replyCount: 1, 528 reactionCount: 0, 529 voteCount: 0, 530 lastActivityAt: now, 531 publishedAt: now, 532 indexedAt: now, 533 isLocked: false, 534 isPinned: i === 0, 535 isModDeleted: false, 536 isAuthorDeleted: false, 537 moderationStatus: 'approved' as const, 538 trustStatus: 'trusted' as const, 539 } 540 }) 541 542 await db.insert(topics).values(topicValues) 543 544 const replyValues = demoTopics.map((t, i) => { 545 const topicRkey = `seed${String(i + 1).padStart(3, '0')}` 546 const topicUri = `at://${did}/forum.barazo.topic.post/${topicRkey}` 547 const topicCid = `bafyreiseed${String(i + 1).padStart(3, '0')}` 548 const replyRkey = `seedreply${String(i + 1).padStart(3, '0')}` 549 return { 550 uri: `at://${did}/forum.barazo.topic.reply/${replyRkey}`, 551 rkey: replyRkey, 552 authorDid: did, 553 content: t.replyContent, 554 rootUri: topicUri, 555 rootCid: topicCid, 556 parentUri: topicUri, 557 parentCid: topicCid, 558 communityDid, 559 cid: `bafyreiseedreply${String(i + 1).padStart(3, '0')}`, 560 reactionCount: 0, 561 voteCount: 0, 562 depth: 1, 563 createdAt: now, 564 indexedAt: now, 565 isAuthorDeleted: false, 566 isModDeleted: false, 567 moderationStatus: 'approved' as const, 568 trustStatus: 'trusted' as const, 569 } 570 }) 571 572 await db.insert(replies).values(replyValues) 573 logger.info( 574 { communityDid, topicCount: topicValues.length, replyCount: replyValues.length }, 575 'Demo content seeded' 576 ) 577 578 const finalName = row.communityName 579 logger.info({ did, communityName: finalName }, 'Community initialized') 580 581 const result: InitializeResult = { 582 initialized: true, 583 adminDid: did, 584 communityName: finalName, 585 } 586 587 if (plcDid) { 588 result.communityDid = plcDid 589 } 590 591 return result 592 } catch (err: unknown) { 593 logger.error({ err, did }, 'Failed to initialize community') 594 throw err 595 } 596 } 597 598 return { getStatus, initialize } 599} 600 601// --------------------------------------------------------------------------- 602// Default page content (markdown) 603// --------------------------------------------------------------------------- 604 605const TERMS_OF_SERVICE_CONTENT = `## Acceptance of terms 606 607By accessing or using Barazo, you agree to be bound by these Terms of Service. If you do not agree to these terms, you may not use the service. Barazo reserves the right to update these terms at any time, with notice provided through the platform. 608 609## Eligibility 610 611You must be at least 16 years old to use Barazo (in accordance with the Dutch implementation of GDPR, UAVG). By using the service, you confirm that you meet this age requirement. Access to mature content may require additional age verification as required by applicable law. 612 613## Account and authentication 614 615Barazo uses the AT Protocol for authentication. You log in using your existing AT Protocol identity (e.g., a Bluesky account). You are responsible for maintaining the security of your AT Protocol account. Barazo does not store your password. 616 617## Content and conduct 618 619You retain ownership of content you post on Barazo. By posting, you grant Barazo a license to display, index, and distribute your content as part of the forum service and via the AT Protocol. 620 621You agree not to post content that: 622 623- Violates applicable laws or regulations. 624- Infringes on the intellectual property rights of others. 625- Contains spam, malware, or deceptive content. 626- Harasses, threatens, or promotes violence against individuals or groups. 627- Contains child sexual abuse material (CSAM). 628 629Community administrators may enforce additional content policies specific to their community. Repeated violations may result in content removal, account restrictions, or bans. 630 631## Content maturity ratings 632 633Communities and categories may be rated for content maturity (Safe for Work, Mature, or Adult). You are responsible for accurately labeling your content. Communities may require age verification to access mature content. New accounts default to safe-mode with mature content hidden. 634 635## Cross-posting 636 637Barazo may cross-post your content to connected platforms (such as Bluesky or Frontpage) when you enable this feature. Cross-posting is optional and can be controlled in your settings. Cross-posted content is subject to the terms of the destination platform. 638 639## Moderation and labels 640 641Your account may be labeled by independent moderation services (such as Bluesky's Ozone). Labels affect posting limits and content visibility. You cannot delete labels applied by labeler services, but you can dispute inaccuracies by contacting us or the labeler service. Community administrators may also apply local moderation overrides. 642 643## AI-generated summaries 644 645Barazo may generate AI-powered summaries of discussion threads. These summaries are anonymized derivative works that do not contain personal data (no usernames or verbatim quotes). AI summaries may persist after individual content is deleted, as they are regenerated from remaining content. Community administrators can disable summary preservation. 646 647## AT Protocol and federation 648 649Barazo is built on the AT Protocol, which is a federated, open network. Content you post may be indexed by other services on the AT Protocol network. Barazo cannot control how third-party services handle your data once it is published via the protocol. 650 651## Termination 652 653Barazo may suspend or terminate your access if you violate these terms. You may stop using the service at any time. Deleting your AT Protocol account or content will trigger removal of indexed data from Barazo (see our Privacy Policy for details). 654 655## Limitation of liability 656 657Barazo is provided "as is" without warranties of any kind. We are not liable for any damages arising from your use of the service, including but not limited to loss of data, service interruptions, or actions taken by community moderators or administrators. 658 659## Governing law 660 661These terms are governed by the laws of the Netherlands. Any disputes arising from these terms will be subject to the exclusive jurisdiction of the courts of the Netherlands. 662 663*These terms were last updated on February 2026.*` 664 665const PRIVACY_POLICY_CONTENT = `## Overview 666 667Barazo is committed to protecting your privacy. This policy explains what personal data we collect, why we collect it, how long we keep it, and what rights you have. Barazo is operated from the Netherlands and complies with the General Data Protection Regulation (GDPR). 668 669## What we collect 670 671When you use Barazo, we process the following data: 672 673- **AT Protocol identifiers** -- your DID (decentralized identifier) and handle, used to identify your account. 674- **Profile information** -- display name and profile data retrieved from your AT Protocol PDS. 675- **Content** -- posts, replies, and reactions you create on the forum, indexed from the AT Protocol firehose. 676- **IP addresses** -- collected for API rate limiting and security purposes. 677- **Authentication cookie** -- a single HTTP-only, Secure, SameSite=Strict refresh token cookie used to maintain your session. Access tokens are held in memory only and never stored in cookies or browser storage. 678- **Moderation records** -- actions taken by moderators on your content or account. 679- **Age declaration** -- stored in the forum database only (deliberately kept off your PDS to avoid broadcasting age data on a public network). 680- **Per-community preferences** -- notification settings and content maturity overrides, stored locally in the forum database (not on your PDS) to protect your browsing patterns. 681 682## What we do not collect 683 684- We do not collect or store your password (authentication is handled via AT Protocol OAuth). 685- We do not collect email addresses unless provided by a community admin for billing. 686- We do not collect payment card details (processed by our payment provider). 687- We do not use tracking cookies or analytics that profile your behavior. 688- We do not use device fingerprinting. 689- We do not load third-party trackers, pixels, or analytics scripts. 690 691## Legal basis 692 693We process your data under the following legal bases (GDPR Art. 6): 694 695- **Contract performance** -- processing necessary to provide the forum service you signed up for. 696- **Legitimate interest** -- indexing public AT Protocol content, spam prevention, platform security, content moderation, and AI-generated discussion summaries. 697 698## Data storage and transfers 699 700Our servers are hosted in the European Union (Hetzner, Germany). We use the following sub-processors: 701 702- Hetzner (EU) -- hosting infrastructure. 703- Bunny.net (EU, Slovenia) -- content delivery network. 704- Stripe (EU-US Data Privacy Framework certified) -- payment processing. 705 706A full sub-processor list is maintained at **barazo.forum/legal/sub-processors**. 707 708## Data retention and deletion 709 710Your indexed data is retained while the source exists on your AT Protocol PDS. When you delete content or your account via the AT Protocol, we process the deletion event immediately: 711 712- Your post is removed from public view and replaced with a "deleted by author" notice. 713- Your personal data (DID, handle, AT Protocol URI) is stripped from the database record. 714- The anonymized content (with no link to your identity) may be retained to preserve community knowledge and enable AI-generated discussion summaries. This anonymized data falls outside GDPR scope (Recital 26) because it can no longer identify you. 715 716You may request full content deletion (including anonymized content) by contacting us directly, independent of AT Protocol signals. We respond to deletion requests within one month (GDPR Art. 12(3)). 717 718Barazo cannot guarantee deletion from external systems such as AT Protocol relays, other AppViews, search engine caches, or web archives. Our reasonable steps include: propagating AT Protocol delete events, submitting Google Search Console removal requests for deleted content URLs, and documenting which systems confirmed deletion. 719 720## AI features 721 722Barazo offers optional AI features including thread summaries, semantic search, and content moderation assistance. Here is how they work: 723 724- **No training on your content.** We do not use member posts to train AI models, and we do not provide member content to others for training. 725- **Local-first processing.** The default AI configuration uses local inference (Ollama) -- your content never leaves the server. Your forum administrator may choose a different AI provider; in that case, content is sent to that provider for processing. 726- **Anonymized summaries.** AI-generated thread summaries are designed to exclude usernames, handles, and verbatim quotes. Summaries capture the discussion's substance, not who said what. Summaries may persist after individual content deletion because they contain no personal data. 727 728## Content labels 729 730We subscribe to content labeling services (such as Bluesky's Ozone) for spam detection and content moderation. Labels applied to your account may affect posting limits and content visibility. Labels are stored by the labeler service, not on your PDS. You can dispute labels by contacting us. 731 732## Your rights 733 734Under the GDPR, you have the right to: 735 736- Access the personal data we hold about you. 737- Rectify inaccurate data. 738- Request erasure of your data (right to be forgotten). 739- Object to processing based on legitimate interest. 740- Data portability (built into the AT Protocol). 741- Lodge a complaint with the Dutch Data Protection Authority (Autoriteit Persoonsgegevens). 742 743To exercise these rights, contact us through our [GitHub issue tracker](https://github.com/singi-labs/barazo-workspace/issues) or via the contact details provided by your community administrator. 744 745## Data breach notification 746 747In the event of a data breach, we will notify the Dutch Data Protection Authority within 72 hours (GDPR Art. 33). For high-risk breaches, we will notify affected users without undue delay via AT Protocol notifications and public announcements. 748 749*This policy was last updated on February 2026.*` 750 751const COOKIE_POLICY_CONTENT = `## Overview 752 753Barazo uses a minimal number of cookies. We do not use tracking cookies, advertising cookies, or third-party analytics cookies. This page explains the cookies we do use and why. 754 755## Cookies we use 756 757Barazo uses a single essential cookie: 758 759| Cookie | Purpose | Duration | Type | 760|--------|---------|----------|------| 761| Refresh token | Keeps you logged in across page reloads by enabling silent access token renewal. | Session | Essential | 762 763## Technical details 764 765The refresh token cookie has the following security properties: 766 767- **HTTP-only** -- the cookie is not accessible to JavaScript, preventing cross-site scripting (XSS) attacks. 768- **Secure** -- the cookie is only sent over HTTPS connections. 769- **SameSite=Strict** -- the cookie is not sent with cross-site requests, preventing cross-site request forgery (CSRF) attacks. 770 771Access tokens (used to authenticate API requests) are held in memory only and are never stored in cookies, localStorage, or sessionStorage. 772 773## What we do not use 774 775- No tracking or advertising cookies. 776- No third-party analytics (Google Analytics, etc.). 777- No social media tracking pixels. 778- No fingerprinting or behavioral profiling. 779 780## Cookie consent 781 782Because we only use a single essential cookie required for the service to function, a cookie consent banner is not required under the ePrivacy Directive (EU Directive 2002/58/EC, Art. 5(3)). Essential cookies that are strictly necessary for the service requested by the user are exempt from the consent requirement. 783 784## Theme preference 785 786Your light/dark mode preference is stored in localStorage (not a cookie). This is a client-side preference that is never sent to our servers. 787 788*This policy was last updated on February 2026.*` 789 790const YOUR_DATA_CONTENT = `This page explains how data works on this forum. This forum is built on the AT Protocol -- the same open network that powers Bluesky. That architecture shapes how your data is stored, shared, and controlled. 791 792## How AT Protocol handles data 793 794AT Protocol is a **public network**. Every post, reply, and reaction is stored on the author's personal data server (PDS) and is readable by anyone. This is not a design choice made by this forum -- it is how the protocol works. The same is true for every application built on AT Protocol. 795 796What "public" means in practice: 797 798- **Anyone can read any PDS.** A developer, a researcher, or another application can query an AT Protocol account and see every record it contains. 799- **The firehose broadcasts everything.** AT Protocol uses a relay network (the "firehose") that streams every new record to anyone who subscribes. Other applications, indexers, and services can receive and store copies in real time. 800 801## Your data lives with you 802 803When you post on this forum, your content is written to your AT Protocol data server (your PDS) -- not to the forum's database. The forum indexes your posts so they appear in threads, but your PDS is the source of truth. 804 805This means: 806 807- Your posts belong to your AT Protocol account, not to any individual forum. 808- If a forum shuts down, your posts still exist on your PDS. 809- You can delete any post at any time, and the forum removes it from its index. 810- No forum admin can edit your words. They can moderate (hide or label content), but they cannot change what you wrote. 811 812Your identity and your content travel with you. 813 814## Your data is public 815 816This is the part most people do not expect, so we want to be clear about it. 817 818When you write a post, a reply, or a reaction on this forum, that record is stored on your PDS and is readable by anyone -- not just the forum you posted on. 819 820- **Anyone can see your posts across all forums.** A person or application can query your AT Protocol account and see every post you have written across every Barazo forum. 821- **Your community memberships are visible.** Every post carries the forum's identifier. If you post in a community, anyone who reads your PDS can see that you participated there. 822- **Your handle is public.** Your AT Protocol handle (e.g., \`jay.bsky.team\` or a custom domain) appears on every post. If your handle contains your real name or a domain you own, your real-world identity is linked to all your activity across the network. This is your choice when you set your handle -- but it is worth considering before you post. 823 824**If you mainly read and browse:** Reading and browsing a forum does not write anything to your PDS. Only posting, replying, and reacting create public records. 825 826## What deletion can and cannot do 827 828When you delete a post: 829 8301. The record is removed from your PDS. 8312. The forum removes it from its index immediately. 8323. AT Protocol sends a deletion event to the relay network. 833 834What nobody can guarantee: 835 836- Other services that subscribed to the firehose may have stored a copy before you deleted it. 837- Search engines may have cached the page. 838- Web archives may have preserved it. 839 840This forum honors every deletion immediately and propagates it across the AT Protocol network. Beyond that, we cannot recall what other parties have already received. This is the same for any content published on a public network. 841 842## You own what you write 843 844You hold the copyright to every post, reply, and piece of content you create on this forum. Barazo does not claim any license over your content. We do not use it for advertising, we do not sell it, and we do not package it as a dataset. 845 846**But copyright is not access control.** Because your posts are public AT Protocol records, anyone on the network can read, index, and store them. Others could collect your public posts into a dataset, use them for research, or feed them to an AI model. Copyright gives you legal standing to challenge misuse -- it does not prevent access to content you published on a public network. 847 848## What this forum collects 849 850This forum's database is an **index**, not a primary store. Here is what the software collects: 851 852**Indexed from your PDS (public data you authored):** 853- Your posts, replies, and reactions -- so they appear in forum threads 854- Your handle and display name -- so other members can see who wrote what 855 856**Generated by the software:** 857- A session cookie (one, HTTP-only, used for authentication -- no tracking cookies) 858- Moderation logs (if your content is reported or actioned) 859- Rate-limiting data (IP-based, not stored long-term) 860 861**Not collected:** 862- No email address 863- No password (AT Protocol OAuth handles authentication) 864- No behavioral analytics, no pageviews, no click tracking 865- No advertising profiles, no device fingerprinting 866- No third-party trackers, pixels, or analytics scripts 867 868For the full legal details, see [Privacy policy](/p/privacy-policy). 869 870## The trade-off, plainly stated 871 872AT Protocol gives you real data ownership: your identity is portable, your content lives on your server, and no platform can hold your data hostage. The trade-off is that "your server" is a public server. Your posts are as public as a personal website -- anyone can read them, and copies may spread beyond your original intent. 873 874We believe this trade-off is worth it. Platforms that promise privacy while locking your data in their database are offering the illusion of control. AT Protocol offers the real thing -- ownership -- with the honest caveat that ownership of a public record means the world can see it. 875 876If you would not put it on a public website, do not post it on a forum built on AT Protocol.` 877 878const ACCESSIBILITY_CONTENT = `## Our commitment 879 880Barazo is committed to ensuring digital accessibility for people with disabilities. We continually improve the user experience for everyone and apply the relevant accessibility standards. 881 882## Conformance status 883 884We aim to conform to the **Web Content Accessibility Guidelines (WCAG) 2.2 Level AA**. These guidelines explain how to make web content more accessible to people with a wide range of disabilities. 885 886## Testing methods 887 888We test accessibility through a combination of methods: 889 890- **Automated testing** using axe-core and ESLint accessibility rules in our continuous integration pipeline. 891- **Keyboard navigation** testing to ensure all interactive elements are reachable and operable without a mouse. 892- **Screen reader** testing with VoiceOver to verify content is properly announced and navigable. 893- **Lighthouse audits** targeting an accessibility score of 95 or higher on all page types. 894 895## Accessibility features 896 897- Semantic HTML with proper heading hierarchy and landmark regions. 898- Skip links for jumping to main content and the reply editor. 899- Keyboard-accessible controls with visible focus indicators. 900- ARIA attributes for dynamic content, dialogs, and tab patterns. 901- Color contrast meeting WCAG AA requirements in both light and dark themes. 902- Pagination as the default for content lists (no infinite scroll). 903- Respects reduced motion preferences via prefers-reduced-motion. 904 905## Known limitations 906 907While we strive for full accessibility, some areas may have limitations: 908 909- User-generated content may not always meet accessibility standards (e.g., images without alt text in posts). 910- Third-party embeds and plugins may have their own accessibility limitations. 911 912## Contact us 913 914If you encounter accessibility barriers on Barazo, please contact us. We take accessibility feedback seriously and will work to address issues promptly. 915 916You can report accessibility issues through our [GitHub issue tracker](https://github.com/singi-labs/barazo-web/issues). Please include the page URL, a description of the issue, and the assistive technology you are using. 917 918*This statement was last updated on February 2026.*`