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

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

Join Chimera →