Barazo AppView backend
barazo.forum
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.*`