Building a Privacy-First Forum Platform
Written by Terrell Flautt on October 8, 2025
· 10 min · forum.snapitsoftware.com
The Chimera Vision
Problem: Discord, Reddit, and traditional forums sacrifice user privacy for convenience.
Solution: Zero-knowledge encrypted forum platform that literally cannot read your messages.
Architecture
React PWA Frontend → CloudFront → API Gateway
↓
Lambda Functions → DynamoDB + S3
↓
PGP 4096-bit End-to-End Encryption
Zero-Knowledge Principle
The server never has access to unencrypted content. All encryption/decryption happens client-side using PGP keys generated in the browser.
Core Features
1. End-to-End PGP Encryption
// Client-side message encryption
import openpgp from 'openpgp';
async function encryptMessage(message, recipientPublicKey) {
const encrypted = await openpgp.encrypt({
message: await openpgp.createMessage({ text: message }),
encryptionKeys: await openpgp.readKey({ armoredKey: recipientPublicKey }),
format: 'armored'
});
return encrypted;
}
async function decryptMessage(encryptedMessage, privateKey, passphrase) {
const message = await openpgp.readMessage({ armoredMessage: encryptedMessage });
const privateKeyObj = await openpgp.decryptKey({
privateKey: await openpgp.readPrivateKey({ armoredKey: privateKey }),
passphrase
});
const { data: decrypted } = await openpgp.decrypt({
message,
decryptionKeys: privateKeyObj
});
return decrypted;
}
2. Ephemeral Messaging
// Auto-delete after 60 seconds
const MESSAGE_TTL = 60;
exports.createMessage = async (event) => {
const { encryptedContent, channelId } = JSON.parse(event.body);
const message = {
id: uuidv4(),
channelId,
encryptedContent, // Server only stores encrypted blobs
createdAt: Date.now(),
ttl: Math.floor(Date.now() / 1000) + MESSAGE_TTL
};
await dynamodb.put({
TableName: 'Messages',
Item: message
}).promise();
// Publish to WebSocket subscribers
await notifyChannel(channelId, message);
return { statusCode: 200, body: JSON.stringify(message) };
};
3. Anonymous Forums
Users don't need accounts to participate. Temporary pseudonymous identities are generated per session.
// Session-based anonymous identity
function generateAnonymousId() {
const sessionId = crypto.randomUUID();
const fingerprint = generateFingerprint();
return {
displayName: `Anon_${sessionId.slice(0, 8)}`,
sessionId,
fingerprint, // For anti-spam, not tracking
publicKey: null // Generated client-side if user wants encryption
};
}
4. Dead Man's Switch
// Automatic message release if user doesn't check in
exports.checkDeadManSwitches = async () => {
const switches = await dynamodb.query({
TableName: 'DeadManSwitches',
FilterExpression: 'lastCheckIn < :threshold',
ExpressionAttributeValues: {
':threshold': Date.now() - (24 * 60 * 60 * 1000) // 24 hours
}
}).promise();
for (const switchItem of switches.Items) {
// Release the encrypted message to designated recipients
await releaseEncryptedMessage(switchItem.messageId, switchItem.recipients);
}
};
5. Whistleblower-Safe Polling
// Zero-knowledge polls with encrypted votes
async function createPoll(question, options, anonymous = true) {
const pollId = uuidv4();
// Generate unique encryption key for this poll
const pollKey = await openpgp.generateKey({
type: 'rsa',
rsaBits: 2048,
userIDs: [{ name: `Poll ${pollId}` }]
});
await dynamodb.put({
TableName: 'Polls',
Item: {
id: pollId,
question: await encryptForPoll(question, pollKey.publicKey),
options: await Promise.all(
options.map(opt => encryptForPoll(opt, pollKey.publicKey))
),
anonymous,
privateKey: pollKey.privateKey, // Only admin can decrypt results
votes: []
}
}).promise();
return pollId;
}
Progressive Web App
Service Worker for Offline Access
// sw.js - Offline-first forum
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
if (response) return response;
return fetch(event.request).then((response) => {
// Cache encrypted messages for offline viewing
if (event.request.url.includes('/api/messages')) {
const responseClone = response.clone();
caches.open('forum-messages-v1').then((cache) => {
cache.put(event.request, responseClone);
});
}
return response;
});
})
);
});
Mobile App (React Native)
Same codebase, native performance. Biometric authentication for key storage.
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
async function storePrivateKey(privateKey) {
const hasAuth = await LocalAuthentication.hasHardwareAsync();
if (hasAuth) {
await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to secure your encryption keys'
});
await SecureStore.setItemAsync('pgp_private_key', privateKey, {
keychainAccessible: SecureStore.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
});
}
}
Security Features
No User Tracking
- No cookies (uses sessionStorage only)
- No IP logging beyond rate limiting
- No analytics or telemetry
- Tor-friendly (works over .onion)
Rate Limiting Without Identity
// Rate limit based on fingerprint, not user ID
function getRateLimitKey(event) {
const fingerprint = event.headers['x-fingerprint'];
const ip = event.requestContext.identity.sourceIp;
// Combine but hash to prevent tracking
return crypto
.createHash('sha256')
.update(`${fingerprint}:${ip}`)
.digest('hex');
}
async function checkRateLimit(key) {
const count = await redis.incr(key);
await redis.expire(key, 60); // 1 minute window
return count <= 10; // 10 requests per minute
}
Deployment
Infrastructure as Code
service: snapit-forum-api
provider:
name: aws
runtime: nodejs18.x
environment:
MESSAGES_TABLE: ${self:service}-messages-${self:provider.stage}
POLLS_TABLE: ${self:service}-polls-${self:provider.stage}
functions:
createMessage:
handler: messages.create
events:
- http:
path: messages
method: post
cors: true
wsConnect:
handler: websocket.connect
events:
- websocket:
route: $connect
wsDisconnect:
handler: websocket.disconnect
events:
- websocket:
route: $disconnect
What Makes Chimera Different
- No server access to content - True zero-knowledge architecture
- No accounts required - Participate anonymously
- Works offline - PWA with service worker caching
- Ephemeral by default - Messages auto-delete
- Whistleblower features - Dead man's switch, secure polling
Tech Stack Summary
- Frontend: React + OpenPGP.js
- Backend: AWS Lambda + API Gateway + WebSocket API
- Database: DynamoDB with TTL
- Storage: S3 for encrypted file attachments
- Mobile: React Native (iOS/Android)