Liking cljdoc? Tell your friends :D

tenant

Multi-tenancy with schema-per-tenant isolation on PostgreSQL. Provides tenant CRUD, schema provisioning, tenant resolution, and cross-library integration.

Requires PostgreSQL. On SQLite, MySQL or H2 the application refuses to boot with :wagoe/tenant configured. A test profile on H2 sets :allow-h2? true under :wagoe/tenant and runs it with tenant rows but no per-tenant schemas.

Key namespaces

NamespacePurpose

wagoe.tenant.core.tenant

Pure functions: slug validation, schema name generation, prepare-tenant, decisions

wagoe.tenant.ports

Protocols: ITenantRepository, ITenantService, ITenantSchemaProvider, ITenantAuditRepository, and memberships and invites (ITenantMembership*, ITenantInvite*)

wagoe.tenant.schema

Malli schemas: Tenant, TenantInput, TenantUpdate, TenantSettings

wagoe.tenant.shell.service

Service layer with observability interceptors

wagoe.tenant.shell.persistence

Database CRUD with case conversion

wagoe.tenant.shell.provisioning

PostgreSQL schema provisioning/deprovisioning

wagoe.tenant.shell.http

REST API handlers (CRUD + provision/suspend/activate, export)

wagoe.tenant.shell.membership-http

Membership and invitation endpoints

wagoe.tenant.shell.module-wiring

Integrant lifecycle (ig/init-key, ig/halt-key!)

Tenant lifecycle

:active     → Created; its PostgreSQL schema is created with it
:suspended  → Access disabled (data preserved)
:deleted    → Soft delete, `deleted-at` set; the schema is kept

Slug and schema naming

;; Slug: lowercase alphanumeric + hyphens, 2–56 chars, so that
;; "tenant_" + slug fits PostgreSQL's 63-byte identifier limit
(tenant/valid-slug? "acme-corp")     ;=> true
(tenant/valid-slug? "ACME")          ;=> false (uppercase)
(tenant/valid-slug? "acme_corp")     ;=> false (underscore)

;; Slug → PostgreSQL schema name
(tenant/slug->schema-name "acme-corp")  ;=> "tenant_acme_corp"

Schema provisioning

(require '[wagoe.tenant.shell.provisioning :as provisioning])

;; Provision: creates PostgreSQL schema and copies table structure
(provisioning/provision-tenant! db-ctx tenant)
;=> {:success? true :schema-name "tenant_acme_corp" :table-count 5}

;; Execute queries in tenant schema context
(provisioning/with-tenant-schema db-ctx "tenant_acme_corp"
  (fn [tx]
    ;; SET search_path TO tenant_acme_corp, public
    (jdbc/execute! tx ["SELECT * FROM orders"])))

HTTP API

# Admin role required
POST   /api/v1/tenants                   # Create tenant
GET    /api/v1/tenants                   # List tenants
GET    /api/v1/tenants/:id               # Get tenant
PUT    /api/v1/tenants/:id               # Update tenant
POST   /api/v1/tenants/:id/provision     # Provision schema
POST   /api/v1/tenants/:id/suspend       # Suspend tenant
POST   /api/v1/tenants/:id/activate      # Activate tenant
DELETE /api/v1/tenants/:id               # Soft delete

# Platform admin
GET    /api/v1/tenants/:id/export        # The tenant's data
POST   /api/v1/tenants/:id/export-jobs   # The same, as a background job

# Memberships
POST   /api/v1/tenants/:tenant-id/memberships       # Invite a user
GET    /api/v1/tenants/:tenant-id/memberships       # List members
GET|PUT|DELETE /api/v1/tenants/:tenant-id/memberships/:id
POST   /api/v1/memberships/:id/accept               # Accept an invitation

Testing

clojure -M:test:test/pg :tenant

Can you improve this documentation? These fine people already did:
thijscreemers & Thijs Creemers
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close