Use Cases (अपडेट: 2/6/2026)

Claude Code से सुरक्षित multi-tenant SaaS बनाना: RLS, auth, billing, jobs और logs

Claude Code से tenant_id, RLS, auth boundary, plan limits, background jobs, safe logs और leak tests लागू करें।

Claude Code से सुरक्षित multi-tenant SaaS बनाना: RLS, auth, billing, jobs और logs

Multi-tenant SaaS का मतलब है कि एक ही application कई customer organizations को serve करता है। Tenant यहां customer data boundary है: company, workspace, client portal या account. Claude Code route, database helper, background job और UI बहुत तेज़ी से बदल सकता है, इसलिए tenant boundary को implementation से पहले साफ लिखना जरूरी है।

सबसे बड़ा incident यह है कि Tenant A का user Tenant B का project, invoice, file, audit event या AI prompt पढ़ ले। Code review में सिर्फwhere tenantIdढूंढना काफी नहीं है। सुरक्षित design मेंtenant_idauthentication, session, database, billing, background jobs, search और logs तक जाता है। फिर database आखिरी guard बनता है, ताकि application query गलती करे तो भी cross-tenant leak न हो।

इस guide में Claude Code को shortcut नहीं, बल्कि secure implementation partner माना गया है। RLS यानी Row Level Security: PostgreSQL की policy जिससे current database context के हिसाब से rows visible या writable होती हैं। Official reference के लिए PostgreSQL Row Security Policies, CREATE POLICY, औरcurrent_setting / set_config पढ़ें।

Related foundation के लिए Claude Code authentication, RBAC guide, और database migration भी देखें। Multi-tenancy इन तीनों को एक साथ जोड़ता है।

पहले isolation model चुनें

Tenant isolation के तीन common models हैं। Claude Code से handler लिखवाने से पहले यह decision करें।

Modelकब use करेंफायदाrisk
Shared database, shared schemaEarly/mid SaaS, कई छोटे customersकम operations cost, analytics आसानtenant filter miss हुआ तो leak
Shared database, schema per tenantकुछ बड़े customers को ज्यादा separation चाहिएpermissions और migration कुछ हद तक अलगschema count बढ़ते ही deployment hard
Database per tenantfinance, health, strict contractbackup, keys, outage अलग रख सकते हैंcost और operations heavy

Practical सवाल है: क्या यह model 100 या 1000 tenants पर चल पाएगा? ज्यादातर small और mid-size SaaS shared database और shared schema से शुरू कर सकते हैं, अगरtenant_id, RLS, audit logs और tenant restore process मजबूत हों। अगर कोई enterprise customer physical isolation मांगता है, तो selected tenants को अलग database में move करने का path पहले से सोचें।

तीन examples देखें। पहला, B2B CRM: companies, contacts, deals और notes tenant-owned हैं। दूसरा, agency client portal: agency staff कई tenants में हो सकता है, लेकिन client user सिर्फ अपना portal देखता है। तीसरा, AI SaaS: prompt runs, tokens, uploaded files और storage tenant-wise count होने चाहिए, नहीं तो billing और plan limit गलत होंगे।

Claude Code को पहले design review prompt दें।

I want to make this SaaS codebase multi-tenant.
Assume a shared PostgreSQL database and shared schema.
Classify tables into tenant-owned tables, global reference tables, and tables that need PostgreSQL RLS.
Dangerous shortcuts are not allowed: no "application WHERE clauses are enough", no full request-body logging, and no background jobs that infer tenant_id later.
Return changed areas, database constraints, test cases, and migration risks.

trusted source से tenant_id propagate करें

Browser से आएx-tenant-idheader या URL parameter को truth न मानें। Safe flow है: host या path से candidate tenant resolve करें, server पर verify करें कि signed-in user उस tenant का member है, फिर verifiedtenantIdको internal services में pass करें।

Auth.js official Extending the Session में session extend करने का तरीका दिखाता है। फिर भी session को छोटा रखें। Session user पहचानता है; tenant membership और role server-side database से check करें, क्योंकि role change, suspension और membership removal हो सकते हैं।

// src/lib/tenant-context.ts
import { z } from "zod";

const hostSchema = z.string().min(1).max(255);

export type SessionUser = {
  id: string;
  email: string;
};

export type TenantContext = {
  tenantId: string;
  userId: string;
  role: "owner" | "admin" | "member" | "viewer";
  requestId: string;
};

type TenantRecord = {
  id: string;
  subdomain: string | null;
  custom_domain: string | null;
};

type MembershipRecord = {
  tenant_id: string;
  user_id: string;
  role: TenantContext["role"];
};

export async function resolveTenantContext(input: {
  request: Request;
  sessionUser: SessionUser | null;
  requestId: string;
  findTenantByHost: (host: string) => Promise<TenantRecord | null>;
  findMembership: (tenantId: string, userId: string) => Promise<MembershipRecord | null>;
}): Promise<TenantContext> {
  if (!input.sessionUser) {
    throw new Response("Unauthorized", { status: 401 });
  }

  const host = hostSchema.parse(new URL(input.request.url).host.toLowerCase());
  const tenant = await input.findTenantByHost(host);
  if (!tenant) {
    throw new Response("Tenant not found", { status: 404 });
  }

  const membership = await input.findMembership(tenant.id, input.sessionUser.id);
  if (!membership) {
    throw new Response("Forbidden for this tenant", { status: 403 });
  }

  return {
    tenantId: tenant.id,
    userId: input.sessionUser.id,
    role: membership.role,
    requestId: input.requestId,
  };
}

इस code की सबसे जरूरी बात है कि यह client-provided tenant को trust नहीं करता। Internal API और worker भी यही rule follow करें। बाहर से आया tenant_id सिर्फ claim है; membership, tenant-bound API key या signed job payload से उसे prove करना होगा।

PostgreSQL RLS को last guard बनाएं

Application filter useful है, लेकिन filter miss होने से breach नहीं होना चाहिए। Shared schema में हर tenant-owned table परtenant_id, foreign keys, tenant-aware indexes और RLS policies चाहिए। PostgreSQL में table owner औरBYPASSRLSrole RLS bypass कर सकते हैं, इसलिए app role protected tables का owner नहीं होना चाहिए। Critical tables परFORCE ROW LEVEL SECURITYलगाएं।

-- db/migrations/20260602_multi_tenant_rls.sql
CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE tenants (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  slug text NOT NULL UNIQUE,
  name text NOT NULL,
  plan text NOT NULL DEFAULT 'starter',
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE app_users (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email text NOT NULL UNIQUE,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE tenant_memberships (
  tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  user_id uuid NOT NULL REFERENCES app_users(id) ON DELETE CASCADE,
  role text NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
  created_at timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (tenant_id, user_id)
);

CREATE TABLE projects (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  name text NOT NULL,
  status text NOT NULL DEFAULT 'active',
  created_by uuid NOT NULL REFERENCES app_users(id),
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX projects_tenant_id_id_idx ON projects (tenant_id, id);
CREATE INDEX tenant_memberships_user_id_tenant_id_idx ON tenant_memberships (user_id, tenant_id);

ALTER TABLE tenant_memberships ENABLE ROW LEVEL SECURITY;
ALTER TABLE tenant_memberships FORCE ROW LEVEL SECURITY;
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
ALTER TABLE projects FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_memberships_isolation
  ON tenant_memberships
  FOR ALL
  USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid)
  WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid);

CREATE POLICY projects_isolation
  ON projects
  FOR ALL
  USING (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid)
  WITH CHECK (tenant_id = nullif(current_setting('app.tenant_id', true), '')::uuid);

Connection pool में tenant context transaction-local रखें। Session-level value reused connection में बच सकती है। इसलिएset_config(..., true)transaction में call करें।

// src/db/tenant-db.ts
import { Pool, PoolClient } from "pg";

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

const uuidPattern =
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

export async function withTenant<T>(
  tenantId: string,
  work: (client: PoolClient) => Promise<T>,
): Promise<T> {
  if (!uuidPattern.test(tenantId)) {
    throw new Error("Invalid tenant id");
  }

  const client = await pool.connect();
  try {
    await client.query("BEGIN");
    await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
    const result = await work(client);
    await client.query("COMMIT");
    return result;
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
}

export async function listProjects(tenantId: string) {
  return withTenant(tenantId, async (db) => {
    const result = await db.query(
      "SELECT id, name, status, created_at FROM projects ORDER BY created_at DESC",
    );
    return result.rows;
  });
}

Example query में जानबूझकरWHERE tenant_id = $1नहीं है। RLS active होने पर PostgreSQL invisible rows filter करेगा। Production में performance के लिए application filter जोड़ सकते हैं, लेकिन RLS हटाना shortcut नहीं है।

auth, billing और jobs को same boundary पर रखें

Auth का सवाल सिर्फ login नहीं है; सवाल है कि यह user इस tenant में क्या कर सकता है। Agency, consultant या support user कई tenants में हो सकते हैं, इसलिए सिर्फuser_idसे record fetch करना unsafe है। Services मेंtenantId, userId, role, requestIdसाथ pass करें।

Billing भी tenant boundary पर निर्भर है। Stripe काusage-based billingusage report करने पर आधारित है, लेकिन app को पहले correct tenant usage count करना होगा। Plan limits UI text नहीं, server-side guard होने चाहिए।

// src/lib/plan-limits.ts
type Plan = "starter" | "growth" | "enterprise";
type Metric = "users" | "projects" | "aiRuns";

const PLAN_LIMITS: Record<Plan, Record<Metric, number>> = {
  starter: { users: 5, projects: 20, aiRuns: 500 },
  growth: { users: 50, projects: 500, aiRuns: 10_000 },
  enterprise: { users: 10_000, projects: 1_000_000, aiRuns: 50_000_000 },
};

export function assertPlanLimit(input: {
  plan: Plan;
  metric: Metric;
  currentUsage: number;
  increment?: number;
}) {
  const nextUsage = input.currentUsage + (input.increment ?? 1);
  const limit = PLAN_LIMITS[input.plan][input.metric];

  if (nextUsage > limit) {
    throw new Response(
      JSON.stringify({
        error: "PLAN_LIMIT_EXCEEDED",
        metric: input.metric,
        limit,
        nextUsage,
      }),
      { status: 402, headers: { "content-type": "application/json" } },
    );
  }
}

Background jobs में leak बहुत common है। Queue payload में सिर्फprojectIdन डालें। VerifiedtenantIdpayload में रखें और worker में फिरwithTenantसे RLS context set करें।

// src/jobs/send-project-digest.ts
import { withTenant } from "../db/tenant-db";

type ProjectDigestJob = {
  tenantId: string;
  projectId: string;
  requestedBy: string;
};

export async function handleProjectDigestJob(job: ProjectDigestJob) {
  return withTenant(job.tenantId, async (db) => {
    const project = await db.query(
      "SELECT id, name FROM projects WHERE id = $1",
      [job.projectId],
    );

    if (project.rowCount !== 1) {
      throw new Error("Project not visible for tenant");
    }

    await db.query(
      "INSERT INTO audit_events (tenant_id, actor_user_id, event_name) VALUES ($1, $2, $3)",
      [job.tenantId, job.requestedBy, "project_digest_sent"],
    );
  });
}

logs में enough data रखें, secrets नहीं

Logs अक्सर weak controls वाली second database बन जाते हैं। OWASP Logging Cheat Sheet event data sanitize करने और sensitive data हटाने पर जोर देता है। Multi-tenant SaaS मेंtenantId, requestId, event name, actor ID और target ID जरूरी हो सकते हैं। Cookie, Authorization, API key, full prompt, full request body और billing address log न करें।

// src/lib/safe-log.ts
type LogLevel = "info" | "warn" | "error";

const SENSITIVE_KEYS = new Set([
  "authorization",
  "cookie",
  "password",
  "token",
  "apiKey",
  "prompt",
  "secret",
]);

function redact(value: unknown): unknown {
  if (!value || typeof value !== "object") return value;
  if (Array.isArray(value)) return value.map(redact);

  return Object.fromEntries(
    Object.entries(value).map(([key, child]) => [
      key,
      SENSITIVE_KEYS.has(key) ? "[REDACTED]" : redact(child),
    ]),
  );
}

export function tenantLog(level: LogLevel, message: string, fields: Record<string, unknown>) {
  const safeFields = redact(fields);
  console[level](JSON.stringify({ message, ...safeFields }));
}

Audit log और operational log अलग रखें। Audit log को बताना चाहिए कि किसने, किस tenant में, किस target पर, कौन सा action किया। Full record diff तभी रखें जब product या compliance सच में मांगता हो।

leak करने की कोशिश वाले tests लिखें

Happy-path tests काफी नहीं हैं। Tenant A से Tenant B का ID access कराएं, worker को mismatched payload दें,app.tenant_idmissing रखें, plan limit exceed करें, और logger को sensitive fields दें।

-- db/tests/rls-smoke-test.sql
\set ON_ERROR_STOP on

BEGIN;

INSERT INTO tenants (id, slug, name)
VALUES
  ('00000000-0000-4000-8000-000000000001', 'alpha', 'Alpha Inc'),
  ('00000000-0000-4000-8000-000000000002', 'beta', 'Beta Inc');

INSERT INTO app_users (id, email)
VALUES ('10000000-0000-4000-8000-000000000001', 'masa@example.com');

INSERT INTO projects (id, tenant_id, name, created_by)
VALUES
  ('20000000-0000-4000-8000-000000000001', '00000000-0000-4000-8000-000000000001', 'Alpha project', '10000000-0000-4000-8000-000000000001'),
  ('20000000-0000-4000-8000-000000000002', '00000000-0000-4000-8000-000000000002', 'Beta project', '10000000-0000-4000-8000-000000000001');

SELECT set_config('app.tenant_id', '00000000-0000-4000-8000-000000000001', true);

DO $$
DECLARE visible_count integer;
BEGIN
  SELECT count(*) INTO visible_count FROM projects;
  IF visible_count <> 1 THEN
    RAISE EXCEPTION 'RLS failed: expected 1 visible project, got %', visible_count;
  END IF;
END $$;

ROLLBACK;
#!/usr/bin/env bash
set -euo pipefail

: "${DATABASE_URL:?DATABASE_URL is required}"

psql "$DATABASE_URL" -f db/migrations/20260602_multi_tenant_rls.sql
psql "$DATABASE_URL" -f db/tests/rls-smoke-test.sql

Claude Code को adversarial acceptance criteria दें।

Add multi-tenant leak tests with these cases:
1. A Tenant A session using a Tenant B projectId returns 403 or 404.
2. A DB query without app.tenant_id returns zero rows or fails closed.
3. A background job without tenantId is rejected.
4. A plan-limit breach returns 402 before the write.
5. Logs never include authorization, cookie, prompt, token, or apiKey.
Explain the implementation that would fail each test, then show the commands and results after the fix.

migration pitfalls से बचें

Existing SaaS में multi-tenancy बाद में जोड़ते समय common गलती है: nullabletenant_idadd करना, release करना, और cleanup बाद में छोड़ देना। Backfill phase में temporary NULL चल सकता है, लेकिन release complete तभी है जबNOT NULL, foreign keys, tenant-aware unique constraints, indexes, RLS और tests पूरे हों।

पांच pitfalls देखें। पहला, global reference data और tenant data mix करना। Country codes global हो सकते हैं; customer settings नहीं। दूसरा, file storage key में tenant context न रखना। तीसरा, search index मेंtenant_idfilter भूलना, चाहे Algolia हो या OpenSearch। चौथा, admin panel के लिए RLS बंद करना। Support access explicit और audited impersonation से हो। पांचवां, single-tenant restore test न करना।

Create a migration plan for converting existing tables to multi-tenancy.
Phase 1: add tenant_id columns and create a mapping for existing records.
Phase 2: write backfill SQL and validation SQL for remaining NULL rows.
Phase 3: add NOT NULL, foreign keys, tenant-aware unique constraints, and indexes.
Phase 4: enable RLS and propose where FORCE ROW LEVEL SECURITY is needed.
Phase 5: test APIs, jobs, logs, and search indexes for tenant leaks.
Include rollback criteria and verification SQL for each phase.

Claude Code के लिए safe prompt

Prompt में tenant boundary लिखें। “जल्दी multi-tenant बना दो” या “admin सब देख सकता है” जैसे prompts dangerous हैं। Safe prompt में scope, forbidden actions और tests हों।

You are implementing safe multi-tenant SaaS behavior.
Allowed areas: src/app, src/lib, db/migrations, and tests.

Requirements:
- Resolve tenant_id on the server; never trust client-provided tenant IDs.
- Use PostgreSQL RLS for every tenant-owned table.
- In pooled connections, call set_config('app.tenant_id', value, true) inside a transaction.
- Propagate tenantId through auth, billing, jobs, logs, and search indexing.
- Add tests that intentionally attempt cross-tenant reads and writes.

Forbidden:
- Disabling RLS.
- Trusting x-tenant-id.
- Logging Authorization, Cookie, API keys, or full prompts.
- Marking the task done without tests.

Final report must include changed files, commands run, failed cases found, and residual risk.

Anthropic कीClaude Code Securitydocumentation permissions, prompt injection और review responsibility पर बात करती है। Sensitive SaaS repo में allowed commands, MCP connections और project rules को reviewable रखें।

CTA और practical handoff

Multi-tenancy flashy feature नहीं है, लेकिन B2B SaaS में trust बेचती है। अगर आप tenant data isolation, audit logs, plan limits, support access और tenant restore समझा सकते हैं, तो security review आसान होता है।

Team adoption के लिए tenant rules कोCLAUDE.mdमें रखें और tests से enforce करें। हर review में पूछें: क्याtenant_idend-to-end गया, क्या RLS mistake रोकता है, क्या jobs scoped हैं, क्या logs secrets नहीं लिखते? Team support के लिएClaude Code training and consulting देखें। Solo buildersfree cheat sheetसे इस checklist को reusable prompts बना सकते हैं।

असल में test करने पर क्या हुआ

Masa ने छोटे CRM sample में यह pattern test किया तो पहला leak API route में नहीं, daily digest worker में निकला। UItenantIdभेज रहा था, लेकिन queue payload में सिर्फprojectIdथा, इसलिए worker गलत tenant में search कर सकता था। Worker को भीwithTenantसे चलाया और missingapp.tenant_idका SQL test जोड़ा, तो bug reproduce और fix हुआ। Claude Code को “happy-path tests add करो” कहने से बेहतर instruction था: “पहले ऐसा test लिखो जहां Tenant A, Tenant B को पढ़ने की कोशिश करे।”

#Claude Code #multi-tenant #SaaS #PostgreSQL #security
मुफ़्त

मुफ़्त PDF: Claude Code cheatsheet

Email डालें और commands, review habits तथा safe workflow वाली एक-page PDF पाएँ.

हम आपका data सुरक्षित रखते हैं और spam नहीं भेजते.

Masa

लेखक के बारे में

Masa

Claude Code workflow और team adoption पर काम करने वाला engineer.