Skhooler: Multi-Tenant Community & Course Platform
A multi-tenant platform for creators to run paid communities, courses and live events with direct Stripe Connect payouts.





The problem and context
Creators and educators who build paid communities frequently stitch together separate services: a forum for conversation, a video platform for courses, a shared calendar for events, an email tool for announcements, and an external payment processor. Members face fragmented logins across disparate tabs, while community owners contend with fragmented customer data and manual access revocation.
Skhooler unifies these workflows into a single platform. A creator can launch a community, configure free or paid membership tiers, publish rich multimedia courses and digital products, schedule events, and monitor real-time analytics. Members participate in live discussion feeds, vote in polls, and observe fellow members coming online in real time.
The central engineering challenge was not standard CRUD operations—it was coordinating payment lifecycles, role-based access control, and distributed real-time state concurrently. A member who stops paying must lose access promptly without data loss. A duplicate or out-of-order webhook must never double-grant access. And a temporary Redis disruption must never crash the primary HTTP API.
I architected and engineered the end-to-end system: database schema design, REST API, asynchronous queue workers, Stripe Connect integration, the React client, and the visual design system.
Stack and architecture
Role: Full-Stack Engineer & UI Designer
Timeline: 2026
Core Stack: React, Node.js, Express, PostgreSQL, Prisma ORM, Redis & BullMQ, Socket.IO, Stripe Connect, Tailwind CSS
The architecture spans approximately 100 endpoints organised across 15 modular domains (auth, groups, posts, courses, products, events, billing, notifications, search, analytics, and integrations), designed around several key resilience patterns:
1. Payment Idempotency & Stripe Connect Direct Charges
Payments use Stripe Connect with direct charges: the community creator acts as the merchant of record, and the platform retains a 10% application fee. Stripe serves as the single source of truth for subscription status, while webhooks drive application state transitions.
Because webhook events can arrive late, out of order, or repeatedly, database updates are completely idempotent:
// membership.service.js: Stripe is the source of truth (simplified)
const PAYING = ['ACTIVE', 'TRIALING', 'PAST_DUE']; // PAST_DUE retains access while Stripe retries
const ENDED = ['CANCELED', 'UNPAID'];
export async function syncSubscription(sub, known = {}) {
// ...resolve userId/groupId from metadata or local records...
const record = await prisma.subscription.upsert({
where: { stripeSubscriptionId: sub.id },
create: { ...fields, userId, groupId, type: 'GROUP_MEMBERSHIP' },
update: fields, // Idempotent: duplicate events are harmless
});
if (PAYING.includes(status)) await restoreMember(groupId, userId);
else if (ENDED.includes(status)) await lapseMember(groupId, userId); // Locked out, not deleted
return record;
}
Idempotent subscription reconciliation ensuring Stripe remains the authoritative source of truth.
Both the webhook listener and the post-checkout client confirmation endpoint route through the same idempotent handler (fulfilCheckout), allowing whichever arrives first to safely process the transaction. A daily automated reconciliation job re-checks subscriptions whose webhooks were potentially dropped during network partitions, issuing renewal reminders at 3 and 10 days.
2. Soft-Lapsing Relational Data Model
The PostgreSQL schema spans over 40 Prisma models. Rather than deleting membership records when a subscription ends, the system marks members as lapsed:
// 40+ Prisma models. A member who stops paying is lapsed, not removed,
// so renewing is a single payment without data loss.
model GroupMember {
id String @id @default(uuid()) @db.Uuid
groupId String @db.Uuid
userId String @db.Uuid
role GroupRole @default(MEMBER) // OWNER | ADMIN | MODERATOR | MEMBER
tierId String? @db.Uuid
lapsedAt DateTime? // Set when subscription lapses
lapseRemindersSent Int @default(0)
@@unique([groupId, userId])
@@index([lapsedAt])
}
Group membership schema with soft-lapsing support to preserve user activity history.
Soft lapsing preserves user discussion history, course progress, and community contributions, enabling members to restore full access with a single renewal payment.
3. Asynchronous Worker Architecture (BullMQ)
Asynchronous workloads run on Redis-backed BullMQ across five dedicated queues: transactional emails, broadcast announcements, webhook ingestion, push notifications, and scheduled cron automations. Queues enforce isolated concurrency limits and rate bounds so that bursty notification dispatches cannot delay payment webhooks. Repeatable cron jobs evaluate event reminders every 30 minutes and execute daily membership reconciliations.
4. Client SPA Architecture
The client is built with React 19, Vite, React Router 7, and Zustand. The application was intentionally architected as a pure Vite SPA rather than a server-rendered application: because Skhooler operates entirely behind authenticated user sessions where every view is stateful, real-time, and dynamic, eliminating Server-Side Rendering (SSR) reduced server resource footprint, simplified WebSocket lifecycle management, and made client-side tab caching instant.
5. Production Hardening & Fail-Open Rate Limiting
- Adaptive Backpressure: An active event-loop lag and process memory monitor intercepts incoming requests and returns HTTP 503 Service Unavailable when under extreme host saturation, preserving core API stability instead of degrading globally.
- Fail-Open Rate Limiting: Sliding-window rate limiting is enforced via Redis. In the event of a transient Redis outage, the rate limiter deliberately fails open—a conscious architectural trade-off prioritising platform availability over strict rate caps during infrastructure incidents.
- Circuit Breakers & Fault Tolerance: External HTTP calls to third-party endpoints use Opossum circuit breakers. Cryptographic security includes hashed refresh tokens and password reset secrets.
- Automated Test Coverage: A Vitest test suite covers critical billing workflows, invite token verification, and role-based access rules.
Design and UX decisions
- Warm, Organic Visual System: Instead of standard cool SaaS grey, Skhooler features a custom visual identity using deep burgundy brand accents, sage-blue secondary highlights, and warm sand neutrals on a cream ground. Layouts use a max 20px radius and a refined typographic pairing of Plus Jakarta Sans and Manrope, defined via Tailwind CSS theme variables.
- Accessibility by Default: Custom focus rings leverage
:focus-visiblewithout intrusive outlines on pointer click, animations respectprefers-reduced-motion, and interactive elements maintain WCAG AA contrast across both light and dark modes. - Role-Aware UI with Safe Defaults: A declarative
useCan(action)hook evaluates permission levels against minimum required roles, strictly denying unlisted capabilities. The persisted client shell deliberately omits role data so that stale localStorage caches never briefly flash moderation controls if a member's role was recently adjusted. - Instant Navigation & Tab Isolation: Community feed tabs cache to localStorage tagged by account ID, wiping on sign-out to prevent data bleeding between shared devices. Routes are code-split and paired with route-level skeleton loaders to eliminate jarring layout shifts.
- Graceful Account Transitions: Lapsed members encountering gated content are smoothly routed to a clear renewal checkout. Community owners and administrators are immune from lockout, while pending join requests, approval flows, and post-payment onboarding questionnaires have dedicated, guided screens.
- Tab-Aware Presence: Client heartbeats pause automatically when browser tabs become hidden, re-announcing presence when users regain focus to minimise unnecessary Redis read/write cycles.
Results
- Deployed and live in production at skhooler.carlasamoah.com, running active Stripe Connect payments.
- Ships the full creator monetisation loop: free and paid tiers, membership upgrades, multimedia courses, digital product delivery, automated 30-minute event reminders, live polls, threaded discussion feeds, and creator analytics.
- Direct Stripe Connect routing ensures creators receive revenues directly while the platform automatically deducts its 10% fee.
- Webhook idempotency and daily reconciliation jobs successfully prevent duplicate grants and access drift. Critical billing transitions, invite logic, and access rules are validated through a Vitest test suite.