Serverless Community Builder
Written by Terrell Flautt on October 9, 2025
· 8 min · Privacy-focused social platform
Design Philosophy
Cleaner than Discord. Simpler than Reddit. More private than everything.
Users should be able to create their own communities without sacrificing privacy or dealing with complex admin panels.
Core Architecture
Multi-Tenant Community System
// Single infrastructure, infinite communities
const COMMUNITY_TABLE = 'Communities';
const CHANNELS_TABLE = 'Channels';
exports.createCommunity = async (event) => {
const { name, description, isPrivate, encryption } = JSON.parse(event.body);
const creatorId = event.requestContext.authorizer.userId;
const community = {
id: generateSlug(name), // forum.site.com/c/my-community
name,
description,
creatorId,
isPrivate,
encryption: encryption || 'none', // 'none', 'e2e', 'server'
members: [creatorId],
roles: {
[creatorId]: 'owner'
},
createdAt: Date.now()
};
// Generate encryption keys if E2E
if (encryption === 'e2e') {
const communityKeys = await generateCommunityKeys();
community.publicKey = communityKeys.publicKey;
// Private key shared only with members via encrypted channel
}
await dynamodb.put({
TableName: COMMUNITY_TABLE,
Item: community
}).promise();
// Create default channels
await createDefaultChannels(community.id);
return {
statusCode: 201,
body: JSON.stringify({ community })
};
};
Channel System
// Discord-style channels with privacy controls
async function createChannel(communityId, channelData) {
const { name, type, permissions } = channelData;
const channel = {
id: uuidv4(),
communityId,
name,
type, // 'text', 'voice', 'poll', 'files'
permissions: permissions || 'all-members',
encrypted: type === 'text', // All text encrypted by default
ephemeral: channelData.ephemeral || false,
createdAt: Date.now()
};
await dynamodb.put({
TableName: CHANNELS_TABLE,
Item: channel
}).promise();
return channel;
}
// Default channel structure
async function createDefaultChannels(communityId) {
const defaults = [
{ name: 'general', type: 'text', permissions: 'all-members' },
{ name: 'announcements', type: 'text', permissions: 'read-only' },
{ name: 'off-topic', type: 'text', permissions: 'all-members' }
];
for (const channel of defaults) {
await createChannel(communityId, channel);
}
}
User Experience
Simple Community Creation Flow
// React component - 3 steps to create community
function CreateCommunityWizard() {
const [step, setStep] = useState(1);
const [community, setCommunity] = useState({
name: '',
description: '',
isPrivate: false,
encryption: 'e2e'
});
const steps = [
{ title: 'Name & Description', component: BasicInfo },
{ title: 'Privacy Settings', component: PrivacySettings },
{ title: 'Create', component: Confirmation }
];
async function handleCreate() {
const response = await fetch('/api/communities', {
method: 'POST',
body: JSON.stringify(community)
});
const { community: created } = await response.json();
window.location.href = `/c/${created.id}`;
}
return (
{steps[step - 1].title}
{React.createElement(steps[step - 1].component, {
data: community,
onChange: setCommunity
})}
{step === 3 && }
);
}
Role-Based Access Control
// Simple but powerful permissions
const ROLES = {
owner: {
canModerate: true,
canInvite: true,
canEditChannels: true,
canEditCommunity: true,
canDeleteCommunity: true
},
moderator: {
canModerate: true,
canInvite: true,
canEditChannels: true,
canEditCommunity: false,
canDeleteCommunity: false
},
member: {
canModerate: false,
canInvite: false,
canEditChannels: false,
canEditCommunity: false,
canDeleteCommunity: false
}
};
function canPerformAction(userId, communityId, action) {
const userRole = getCommunityRole(userId, communityId);
return ROLES[userRole][action] === true;
}
Privacy Features
1. Private Communities
// Invite-only with encrypted invites
exports.createInvite = async (event) => {
const { communityId, inviteeEmail } = JSON.parse(event.body);
const inviterId = event.requestContext.authorizer.userId;
// Generate one-time invite token
const inviteToken = crypto.randomBytes(32).toString('hex');
const invite = {
token: inviteToken,
communityId,
inviterId,
inviteeEmail,
expiresAt: Date.now() + (7 * 24 * 60 * 60 * 1000), // 7 days
used: false
};
await dynamodb.put({
TableName: 'Invites',
Item: invite
}).promise();
// Send encrypted invite link
const inviteLink = `https://forum.site.com/invite/${inviteToken}`;
await sendEncryptedEmail(inviteeEmail, inviteLink);
return { statusCode: 200 };
};
2. Content Moderation Without Surveillance
// Community-driven moderation with zero knowledge
exports.reportContent = async (event) => {
const { messageId, reason } = JSON.parse(event.body);
// Create encrypted report that only moderators can decrypt
const moderatorKeys = await getCommunityModeratorKeys(communityId);
const encryptedReport = await encryptForModerators({
messageId,
reason,
encryptedContent: message.encryptedContent, // Pass through encrypted
reportedAt: Date.now()
}, moderatorKeys);
await dynamodb.put({
TableName: 'Reports',
Item: {
id: uuidv4(),
communityId,
encryptedReport,
status: 'pending'
}
}).promise();
// Notify moderators via WebSocket
await notifyModerators(communityId, 'new_report');
};
3. Anonymous Participation Option
// Users can choose anonymity per-community
function joinCommunityAnonymously(communityId) {
const anonIdentity = {
displayName: generateAnonymousName(),
communityId,
publicKey: null, // Generate if they want encryption
persistent: false // Resets on session end
};
sessionStorage.setItem(
`anon_${communityId}`,
JSON.stringify(anonIdentity)
);
return anonIdentity;
}
Real-Time Features
WebSocket Message Broadcasting
// Scalable real-time messaging
exports.broadcastMessage = async (event) => {
const { connectionId } = event.requestContext;
const { channelId, encryptedContent } = JSON.parse(event.body);
// Get all channel subscribers
const subscribers = await getChannelSubscribers(channelId);
// Broadcast to all connections except sender
const apiGateway = new AWS.ApiGatewayManagementApi({
endpoint: process.env.WEBSOCKET_ENDPOINT
});
const message = {
type: 'message',
channelId,
encryptedContent,
timestamp: Date.now()
};
await Promise.all(
subscribers
.filter(sub => sub.connectionId !== connectionId)
.map(sub =>
apiGateway.postToConnection({
ConnectionId: sub.connectionId,
Data: JSON.stringify(message)
}).promise()
)
);
};
Scaling Strategy
Free Tier Design
- Up to 1,500 users per community
- Unlimited communities
- 30-day message retention
- Basic encryption (server-side)
DynamoDB Optimization
// Efficient queries with GSI
const MESSAGES_GSI = 'ChannelTimestampIndex';
async function getChannelMessages(channelId, limit = 50) {
const result = await dynamodb.query({
TableName: 'Messages',
IndexName: MESSAGES_GSI,
KeyConditionExpression: 'channelId = :channelId',
ExpressionAttributeValues: {
':channelId': channelId
},
ScanIndexForward: false, // Latest first
Limit: limit
}).promise();
return result.Items;
}
Mobile App
React Native + Expo
// Push notifications for mentions
import * as Notifications from 'expo-notifications';
async function subscribeToPushNotifications(userId, communityId) {
const token = await Notifications.getExpoPushTokenAsync();
await fetch('/api/notifications/subscribe', {
method: 'POST',
body: JSON.stringify({
userId,
communityId,
pushToken: token.data,
platform: Platform.OS
})
});
}
// Handle encrypted notifications
Notifications.addNotificationReceivedListener(async (notification) => {
const { encryptedContent } = notification.request.content.data;
// Decrypt using stored private key
const privateKey = await SecureStore.getItemAsync('pgp_private_key');
const decrypted = await decryptMessage(encryptedContent, privateKey);
// Show decrypted notification
await Notifications.presentNotificationAsync({
title: 'New Message',
body: decrypted
});
});
What's Next
- Voice channels with WebRTC
- File sharing with E2E encryption
- Custom bots and integrations
- Blockchain-based governance