prax-orm 0.11.0

A next-generation, type-safe ORM for Rust inspired by Prisma
Documentation
---
import DocsLayout from '../../layouts/DocsLayout.astro';
import CodeBlock from '../../components/CodeBlock.astro';

const taskLocalCode = `use prax_query::tenant::with_tenant;

// Zero-allocation tenant context
with_tenant("tenant-123", async {
    // All code here sees tenant-123
    let users = client.user().find_many().exec().await?;
    Ok(())
}).await?;`;

const rlsCode = `use prax_query::tenant::{RlsManager, RlsConfig};

let rls = RlsManager::new(
    RlsConfig::new("tenant_id")
        .with_session_variable("app.current_tenant")
        .add_tables(["users", "orders", "products"])
);

// One-time setup (creates RLS policies)
conn.execute_batch(&rls.setup_sql()).await?;

// Per-request: just set the tenant
conn.execute(&rls.set_tenant_local_sql("tenant-123"), &[]).await?;

// All queries now automatically filtered by tenant_id`;

const cacheCode = `use prax_query::tenant::ShardedTenantCache;

// High-concurrency sharded cache
let cache = ShardedTenantCache::high_concurrency(10_000);

// Get or fetch tenant context.
// Returns Option<TenantContext> — the fetch closure must produce
// Option<TenantContext> (not a Result).
let ctx = cache.get_or_fetch(&tenant_id, || async {
    db_lookup_tenant(&tenant_id).await
}).await;

let Some(ctx) = ctx else {
    // Unknown or expired tenant — reject the request
    return;
};`;

const poolCode = `use prax_query::tenant::{TenantId, TenantPoolManager};

// Per-tenant connection pools.
//
// NOTE: TenantPoolManager is currently scaffolding for driver
// integration — it tracks pool entries, statistics, and eviction,
// but does not yet open driver connections itself.
let manager = TenantPoolManager::builder()
    .per_tenant(100, 10) // up to 100 pools, 10 connections each
    .build();

// Get (or lazily create) the pool entry for a tenant
let entry = manager.get_or_create(&TenantId::new("tenant-123"));
let entry_stats = entry.stats();

// Fleet-wide bookkeeping
let global = manager.global_stats();
let active = manager.active_pools();

// Periodically reap idle pools
let evicted = manager.evict_expired();`;

const preparedCode = `use prax_query::tenant::{StatementCache, StatementKey, TenantId};

// Generic over the driver's prepared-statement type: StatementCache<S>.
// Global cache holding up to 1,000 statements.
let cache = StatementCache::global(1_000);

// Check the cache before preparing
let key = StatementKey::from_sql("SELECT * FROM users WHERE id = $1");
let stmt = match cache.get(&key) {
    Some(stmt) => stmt,
    None => {
        let stmt = conn.prepare(&key.sql).await?;
        cache.insert(key.clone(), stmt.clone());
        stmt
    }
};

// Per-tenant mode: isolated caches, bounded per tenant
let tenant_cache = StatementCache::per_tenant(100, 50);
let tenant_id = TenantId::new("tenant-123");
tenant_cache.insert_for_tenant(&tenant_id, key.clone(), stmt.clone());
let cached = tenant_cache.get_for_tenant(&tenant_id, &key);`;

const middlewareCode = `use prax_query::tenant::{TenantConfig, TenantContext, TenantMiddleware, with_tenant};

// Row-level SQL filtering middleware
let middleware = TenantMiddleware::new(TenantConfig::row_level("tenant_id"));

// SELECT * FROM users WHERE active = true OR admin = true
// is rewritten to:
//   SELECT * FROM users
//   WHERE tenant_id = 'tenant-123' AND (active = true OR admin = true)
//
// INSERT gets the tenant column auto-injected; a pre-existing tenant
// value must match the current tenant exactly. UPDATE / DELETE are
// validated to still reference the tenant column after rewriting.

// Per request, resolve the tenant task-locally — with_tenant takes
// precedence over the middleware-wide slot:
with_tenant("tenant-123", async {
    // every query executed here is filtered to tenant-123
}).await;

// Single-threaded escape hatch: a process-wide slot shared by every
// clone of the middleware (concurrent requests would overwrite each
// other's tenant — prefer with_tenant).
middleware.set_tenant(TenantContext::new("tenant-123"));`;

const jwtCode = `use prax_query::tenant::{JwtClaimExtractor, TenantExtractor};

// Renamed in v0.11: JwtClaimExtractor -> UnverifiedJwtClaimExtractor.
// The old name remains as an alias until the 0.12 breaking release.
//
// CRITICAL: this extractor performs NO signature verification — it
// only base64url-decodes the JWT payload and reads the claim. The
// token MUST already have been authenticated by a verifying middleware
// upstream; used on its own, any caller can forge any tenant id.
let extractor = JwtClaimExtractor::default_claim(); // "tenant_id" claim

// Only call after your auth middleware has verified the token:
if let Some(tenant_id) = extractor.extract(&request_headers) {
    // ... proceed with the verified tenant
}`;
---

<DocsLayout title="Multi-Tenancy - Prax ORM">
  <article class="max-w-4xl mx-auto px-6 py-12">
    <header class="mb-12">
      <h1 class="text-4xl font-bold mb-4">Multi-Tenancy</h1>
      <p class="text-xl text-muted">
        High-performance multi-tenant support with zero-allocation context and RLS integration.
      </p>
    </header>

    <div class="space-y-12">
      <section>
        <h2 class="text-2xl font-semibold mb-4">Isolation Strategies</h2>
        <div class="grid gap-4 mb-6">
          <div class="p-4 rounded-lg bg-surface border border-border">
            <h3 class="font-semibold mb-1">Row-Level Security (RLS)</h3>
            <p class="text-sm text-muted">Database-enforced filtering. Best performance, shared schema.</p>
          </div>
          <div class="p-4 rounded-lg bg-surface border border-border">
            <h3 class="font-semibold mb-1">Schema-Based</h3>
            <p class="text-sm text-muted">Separate schema per tenant. Good isolation, more complex migrations.</p>
          </div>
          <div class="p-4 rounded-lg bg-surface border border-border">
            <h3 class="font-semibold mb-1">Database-Based</h3>
            <p class="text-sm text-muted">Separate database per tenant. Maximum isolation, highest overhead.</p>
          </div>
        </div>
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Zero-Allocation Tenant Context</h2>
        <p class="text-muted mb-4">Use task-local storage for zero-heap-allocation tenant propagation in async code.</p>
        <CodeBlock code={taskLocalCode} lang="rust" />
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">PostgreSQL Row-Level Security</h2>
        <p class="text-muted mb-4">Best performance option. Database handles all filtering automatically.</p>
        <CodeBlock code={rlsCode} lang="rust" />
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Sharded Tenant Cache</h2>
        <p class="text-muted mb-4">High-concurrency LRU cache with reduced lock contention.</p>
        <CodeBlock code={cacheCode} lang="rust" />
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Per-Tenant Connection Pools</h2>
        <p class="text-muted mb-4">Isolated connection resources per tenant. <em>Scaffolding API — placeholder for driver integration.</em></p>
        <CodeBlock code={poolCode} lang="rust" />
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Statement Caching</h2>
        <p class="text-muted mb-4">Reuse prepared statements across requests for better performance.</p>
        <CodeBlock code={preparedCode} lang="rust" />
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">TenantMiddleware (Row-Level SQL Filtering)</h2>
        <p class="text-muted mb-4">
          <code class="text-primary-400">TenantMiddleware</code> rewrites every query to enforce tenant
          isolation at the SQL level — a complement to (or substitute for) database-native RLS.
        </p>
        <CodeBlock code={middlewareCode} lang="rust" />
        <div class="mt-4 p-4 rounded-lg bg-surface border border-border">
          <h3 class="font-semibold mb-2">v0.11 Hardening Behavior</h3>
          <ul class="text-sm text-muted list-disc list-inside space-y-1">
            <li>The tenant value is validated against the column type before interpolation: string ids must match <code>^[A-Za-z0-9_:-.@]+$</code>, UUID columns require a parseable UUID, integer columns an i64.</li>
            <li>An existing <code>WHERE</code> predicate is parenthesized — <code>WHERE a OR b</code> cannot bypass the filter.</li>
            <li>The injected clause always lands before <code>GROUP BY</code> / <code>HAVING</code> / <code>WINDOW</code> / <code>FETCH</code>.</li>
            <li>Fail-closed: CTE-wrapped <code>SELECT</code>, <code>MERGE</code>, <code>REPLACE</code>, and unrecognized statement shapes are rejected loudly instead of passing through unfiltered.</li>
            <li>Task-local resolution (<code>with_tenant</code>) takes precedence over the shared middleware slot.</li>
          </ul>
        </div>
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Deriving Tenants from JWT Claims</h2>
        <p class="text-muted mb-4">
          The recommended way to derive a tenant server-side from auth tokens — with one critical caveat.
        </p>
        <CodeBlock code={jwtCode} lang="rust" />
        <div class="mt-4 p-4 rounded-lg bg-surface border border-red-500/30">
          <p class="text-sm text-red-400">
            <strong>Security warning:</strong> the JWT extractor performs <strong>no signature verification</strong>.
            It must sit behind a verifying auth middleware; never trust a tenant id decoded from an
            unverified token.
          </p>
        </div>
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">v0.11 Deprecations</h2>
        <ul class="text-sm text-muted list-disc list-inside space-y-1">
          <li><code>ColumnType::format_value</code> → use <code>try_format_value</code>, which validates the tenant id instead of interpolating it unchecked.</li>
          <li><code>current_tenant_id_str()</code> (always returns an empty string) → use <code>current_tenant_id()</code>.</li>
          <li><code>JwtClaimExtractor</code> → renamed <code>UnverifiedJwtClaimExtractor</code> (alias kept until 0.12).</li>
        </ul>
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Performance Comparison</h2>
        <div class="overflow-x-auto">
          <table class="w-full text-sm">
            <thead>
              <tr class="border-b border-border">
                <th class="text-left py-3 px-4">Strategy</th>
                <th class="text-left py-3 px-4">Isolation</th>
                <th class="text-left py-3 px-4">Performance</th>
                <th class="text-left py-3 px-4">Complexity</th>
              </tr>
            </thead>
            <tbody class="divide-y divide-border">
              <tr>
                <td class="py-3 px-4">RLS</td>
                <td class="py-3 px-4">Good</td>
                <td class="py-3 px-4 text-green-400">Excellent</td>
                <td class="py-3 px-4 text-green-400">Low</td>
              </tr>
              <tr>
                <td class="py-3 px-4">Schema-Based</td>
                <td class="py-3 px-4">Better</td>
                <td class="py-3 px-4 text-yellow-400">Good</td>
                <td class="py-3 px-4 text-yellow-400">Medium</td>
              </tr>
              <tr>
                <td class="py-3 px-4">Database-Based</td>
                <td class="py-3 px-4 text-green-400">Maximum</td>
                <td class="py-3 px-4 text-yellow-400">Good</td>
                <td class="py-3 px-4 text-red-400">High</td>
              </tr>
            </tbody>
          </table>
        </div>
      </section>

      <section>
        <h2 class="text-2xl font-semibold mb-4">Best Practices</h2>
        <div class="space-y-4">
          <div class="p-4 rounded-lg bg-surface border border-green-500/30">
            <h3 class="font-semibold text-green-400 mb-1">✅ Do</h3>
            <ul class="text-sm text-muted list-disc list-inside space-y-1">
              <li>Derive tenant ID from <em>verified</em> auth tokens server-side (see the JWT caveat above)</li>
              <li>Use RLS for PostgreSQL when possible</li>
              <li>Cache tenant lookups with TTL</li>
              <li>Test tenant isolation thoroughly</li>
            </ul>
          </div>
          <div class="p-4 rounded-lg bg-surface border border-red-500/30">
            <h3 class="font-semibold text-red-400 mb-1">❌ Don't</h3>
            <ul class="text-sm text-muted list-disc list-inside space-y-1">
              <li>Trust client-provided tenant IDs</li>
              <li>Skip RLS policy verification</li>
              <li>Share connections across tenants without RLS</li>
            </ul>
          </div>
        </div>
      </section>
    </div>
  </article>
</DocsLayout>