Fourteen SystemsFourteen Systems

Architecture deep-dive

How SaaSCoreX works under the hood

This page shows the actual enforcement layers that execute before state changes occur. All snippets are production code — not pseudocode, not “simplified examples.”

10 subsystems. All server-enforced. All wired together.

RBAC

Typed permission enforcement

13 action types. Three role tiers. Every mutation guarded server-side. No UI-only protection.

packages/core/src/teams/permissions.ts

export type OrgAction =
  | "org.update"
  | "org.delete"
  | "member.invite"
  | "member.remove"
  | "member.update"
  | "member.list"
  | "project.create"
  | "project.update"
  | "project.delete"
  | "billing.manage"
  | "apiKey.create"
  | "apiKey.delete"
  | "audit.read";

const ROLE_PERMISSIONS: Record<MemberRole, Set<OrgAction>> = {
  OWNER: new Set([ /* all 13 actions */ ]),
  ADMIN: new Set([ /* 9 actions */ ]),
  MEMBER: new Set([ /* 4 actions */ ]),
};

export function requirePermission(
  role: MemberRole, action: OrgAction
): void {
  if (!can(role, action)) {
    throw new PermissionDeniedError(role, action);
  }
}
  • OrgAction union type catches typos at compile time
  • requirePermission() throws before any write executes
  • Role-permission matrix is a data structure, not scattered if-checks

tenant-isolation

Multi-tenant isolation

Every database query scoped to the active org. Cross-tenant access is structurally impossible.

packages/core/src/auth/session.ts

export async function requireOrg() {
  const session = await requireAuth();
  if (!session.activeOrgId) {
    throw new Error(
      "Session has no activeOrgId."
    );
  }
  return session;
}

// Every query is scoped to the active org
const projects = await db.project.findMany({
  where: { orgId: session.activeOrgId },
});
  • requireOrg() guarantees an activeOrgId before any data access
  • Session is cached per request — safe to call multiple times
  • No org context = error, not silent fallthrough

Billing

Entitlement-driven billing

Usage limits enforced at the function level. Plan switching with automatic Stripe proration. No stale permission caches.

packages/core/src/billing/entitlements.ts

export async function checkLimit(
  orgId: string,
  limitKey: keyof PlanEntitlements,
  currentUsage: number,
): Promise<{ allowed: boolean; limit: number; usage: number }> {
  const entitlements = await getEntitlements(orgId);
  const limit = entitlements[limitKey] as number;

  return {
    allowed: currentUsage < limit,
    limit,
    usage: currentUsage,
  };
}

export async function requireEntitlement(
  orgId: string,
  check: (e: PlanEntitlements) => boolean,
  message = "This feature requires an upgrade.",
): Promise<PlanEntitlements> {
  const entitlements = await getEntitlements(orgId);
  if (!check(entitlements)) {
    throw new EntitlementRequiredError(message);
  }
  return entitlements;
}
  • checkLimit() compares live usage against plan entitlements
  • requireEntitlement() throws before the operation runs
  • switchSubscriptionPlan() handles upgrade/downgrade with proration
  • No subscription defaults to free-tier entitlements

Audit

Structured audit logging

24-action typed taxonomy. Query API with date range and action filters. CSV export. Automatic retention policies.

packages/core/src/audit/service.ts

export type AuditAction =
  | "user.login"    | "user.logout"    | "user.delete"
  | "user.update"
  | "org.create"    | "org.update"     | "org.delete"
  | "member.invite" | "member.accept_invite"
  | "member.revoke_invite" | "member.change_role"
  | "member.remove" | "member.leave"
  | "billing.checkout"
  | "billing.subscription_created"
  | "billing.subscription_updated"
  | "billing.subscription_cancelled"
  | "billing.payment_failed"
  | "admin.impersonate" | "admin.user_update"
  | "resource.create" | "resource.update" | "resource.delete"
  | "resource.read" | "user.sessions_invalidated";

export async function audit(input: AuditLogInput): Promise<void> {
  if (!isFeatureEnabled("auditLog")) return;

  try {
    await db.auditLog.create({
      data: {
        actorId: input.actorId ?? null,
        orgId: input.orgId ?? null,
        action: input.action,
        targetType: input.targetType ?? null,
        targetId: input.targetId ?? null,
        meta: input.meta ?? undefined,
        ipAddress: input.ipAddress ?? null,
        userAgent: input.userAgent ?? null,
      },
    });
  } catch {
    // Never throws — audit failures don't disrupt the primary flow
  }
}
  • audit() is a fire-and-forget call — never breaks the primary flow
  • Feature-gated: no-op when auditLog is disabled
  • Paginated query API with actor, action, and date range filters
  • purgeOldLogs() called by weekly background job (90-day default)

Features

Three-level feature gating

Disable a subsystem and it vanishes: actions throw, routes 404, UI hides. No dead code, no phantom routes.

packages/features-runtime/src/

// 1. Server actions — throw if disabled
export function assertFeatureEnabled(flag: FeatureFlag): void {
  if (!isFeatureEnabled(flag)) {
    throw new FeatureDisabledError(flag);
  }
}

// 2. Route handlers — return 404 if disabled
export function withFeatureGuard(flag: FeatureFlag, handler) {
  return async (...args) => {
    if (!isFeatureEnabled(flag)) {
      return new NextResponse(null, { status: 404 });
    }
    return handler(...args);
  };
}

// 3. React components — hide UI if disabled
export function FeatureGate({ flag, children, fallback = null }) {
  if (!isFeatureEnabled(flag)) return <>{fallback}</>;
  return <>{children}</>;
}
  • assertFeatureEnabled() — server actions and mutations
  • withFeatureGuard() — route handlers return 404
  • FeatureGate — React component hides nav and UI blocks
  • Config is a plain TypeScript object — no external service needed

API Keys

API key model

SHA-256 hashed storage. Plaintext shown once at creation. Prefix for identification. Expiry and revocation built in.

packages/core/src/api-keys/service.ts

function generateKey() {
  const plaintext = `sk_${randomBytes(32).toString("hex")}`;
  const prefix = plaintext.slice(0, 11); // "sk_" + first 8 hex
  const keyHash = createHash("sha256")
    .update(plaintext).digest("hex");
  return { plaintext, prefix, keyHash };
}

export async function createApiKey(orgId, userId, name) {
  const { plaintext, prefix, keyHash } = generateKey();

  const record = await db.apiKey.create({
    data: { orgId, userId, name, keyHash, prefix },
  });

  // plaintext shown once — never stored
  return { key: plaintext, record };
}

export async function validateApiKey(plaintext) {
  const keyHash = hashKey(plaintext);
  const key = await db.apiKey.findUnique({
    where: { keyHash },
  });
  if (!key || key.revokedAt) return null;
  if (key.expiresAt && key.expiresAt < new Date()) return null;
  return { userId: key.userId, orgId: key.orgId };
}
  • sk_ prefix for easy identification in logs
  • Only the hash is stored — plaintext is never persisted
  • validateApiKey() checks revocation and expiry before authorizing
  • lastUsedAt updated asynchronously to avoid blocking requests

Jobs

Background jobs

Billing reconciliation, audit purge, webhook retry, data export. Scheduled and event-driven via Inngest with automatic retries.

packages/jobs/src/functions/

// Nightly billing reconciliation — 3 AM daily
export const billingReconciliation = inngest.createFunction(
  { id: "billing-reconciliation" },
  { cron: "0 3 * * *" },
  async ({ step }) => {
    if (!isFeatureEnabled("billing")) {
      return { skipped: true, reason: "billing disabled" };
    }
    return step.run("reconcile-subscriptions", () =>
      reconcileSubscriptions()
    );
  },
);

// Audit log purge — Sunday 4 AM, 90-day retention
// Webhook retry — every 15 minutes
// Data export — event-driven, emailed as JSON
  • Billing reconciliation heals Stripe↔DB drift from missed webhooks
  • Audit purge enforces 90-day retention on a weekly schedule
  • Data export gathers org data and emails as JSON attachment
  • Feature-gated: jobs skip gracefully when their subsystem is off

Webhooks

Webhook idempotency

State machine prevents double-processing. RECEIVED → PROCESSING → PROCESSED/FAILED. Race-condition safe.

packages/core/src/billing/webhook-handler.ts

export async function processWebhookEvent(
  event: Stripe.Event
): Promise<void> {
  // Idempotency: deduplicate by stripeEventId
  const existing = await db.webhookEvent.findUnique({
    where: { stripeEventId: event.id },
  });
  if (existing?.status === "PROCESSED") return;
  if (existing?.status === "PROCESSING") return;

  // Claim the event: RECEIVED → PROCESSING
  const webhookEvent = await db.webhookEvent.upsert({
    where: { stripeEventId: event.id },
    create: { stripeEventId: event.id, status: "PROCESSING", ... },
    update: { status: "PROCESSING", attempts: { increment: 1 } },
  });

  try {
    await handleEvent(event);
    // PROCESSING → PROCESSED
    await db.webhookEvent.update({
      where: { id: webhookEvent.id },
      data: { status: "PROCESSED", processedAt: new Date() },
    });
  } catch (error) {
    // PROCESSING → FAILED (retryable)
    await db.webhookEvent.update({
      where: { id: webhookEvent.id },
      data: { status: "FAILED", error: error.message },
    });
    throw error; // Return 500 for Stripe retry
  }
}
  • Deduplication by stripeEventId — events processed exactly once
  • PROCESSING state prevents concurrent instances from racing
  • FAILED events are retried by background job, not lost
  • Error details stored for debugging — not silently swallowed

Rate Limiting

Default-on rate limiting

Memory adapter for single-instance deploys. Redis adapter activates automatically when REDIS_URL is set. No code changes needed.

packages/core/src/observability/rate-limit.ts

export const rateLimits = {
  auth:   { maxRequests: 10,  windowSec: 60 },
  admin:  { maxRequests: 30,  windowSec: 60 },
  api:    { maxRequests: 100, windowSec: 60 },
  action: { maxRequests: 20,  windowSec: 60 },
} as const;

export async function requireRateLimit(
  key: string, config: RateLimitConfig
): Promise<void> {
  const limiter = await getRateLimiter();
  const result = await limiter.check(key, config);
  if (!result.allowed) {
    throw new RateLimitExceededError(result.resetAt);
  }
}

// Auto-detects REDIS_URL → Redis adapter
// No REDIS_URL → in-memory (zero deps)
  • Preset configurations for auth, admin, API, and action routes
  • requireRateLimit() throws before the operation executes
  • Auto-detects Redis for multi-instance deployments
  • Memory store has automatic cleanup and size limits

REST API

21-endpoint REST API

Full CRUD API with OpenAPI 3.1 spec, consistent response envelopes, CORS, pagination, and per-endpoint rate limiting. Typed errors map to HTTP status codes through a centralized handler.

apps/web/src/app/api/v1/

// Consistent response envelope
type ApiResponse<T> =
  | { data: T }
  | { error: { code: ErrorCode; message: string; requestId: string } };

// Every route follows the same pattern:
export async function GET(request: Request) {
  const auth = await authenticateRequest(request);     // 1. Authenticate
  requirePermission(auth.role, "member.list");         // 2. Authorize
  await requireRateLimit(`api:${auth.orgId}`, api);    // 3. Rate limit

  const members = await listMembers(auth.orgId, {      // 4. Org-scoped query
    page, pageSize
  });

  return json({                                        // 5. Envelope response
    data: { items: members, meta: { page, pageSize, total, totalPages } }
  }, { headers: { ...corsHeaders(request), "X-Request-Id": getRequestId() } });
}

// Centralized typed error → HTTP response mapping
export function mapErrorToResponse(request, error) {
  if (error instanceof NotFoundError)       return fail(request, "NOT_FOUND", 404);
  if (error instanceof ValidationError)     return fail(request, "VALIDATION_FAILED", 422);
  if (error instanceof ConflictError)       return fail(request, "CONFLICT", 409);
  if (error instanceof BillingStateError)   return fail(request, "BAD_REQUEST", 400);
  if (error instanceof LimitExceededError)  return fail(request, "LIMIT_EXCEEDED", 403);
  return null; // Unknown error — caller re-throws
}
  • OpenAPI 3.1 spec served at /api/v1/openapi.json
  • Shared envelope: { data } for success, { error } with code, message, and requestId for failures
  • CORS support with configurable allowed origins
  • Pagination with page, pageSize, total, and totalPages metadata
  • Per-endpoint rate limiting with Retry-After and X-RateLimit headers
  • Typed errors (NotFoundError, ValidationError, ConflictError, etc.) map to HTTP codes via centralized handler

What you’d have to build yourself

Typical starter vs. SaaSCoreX

Most starters give you a login page and a Stripe button. Everything below the line is what you’d wire yourself.

CapabilityTypical StarterSaaSCoreX
Auth (login, OAuth)
Stripe checkout button
Role enum
Two-factor auth (TOTP + backup codes)
Typed RBAC (13 server-enforced actions)
Multi-tenant org isolation
Entitlement-driven billing enforcement
Plan switching with Stripe proration
Webhook idempotency state machine
Stripe↔DB nightly reconciliation
Structured audit logging (24 actions)
Notification preferences (per-user)
GDPR-style data export
Three-level feature gating
API key management (SHA-256 hashed)
Background jobs (Inngest)
21-endpoint REST API with OpenAPI 3.1
Rate limiting (memory + Redis)
E2E billing tests (Playwright)

SaaSCoreX is designed to be the foundation you ship real revenue on.

Every system on this page is production code. One-time purchase. Full source access. No subscription.