Overview
Torn uses a tenant-per-schema architecture where each company gets its own PostgreSQL schema for data isolation. This guide covers tenant provisioning, user management, and configuration.
Understanding the Multi-Tenant Architecture
Each tenant (company) has:
A unique entry in the public.tenants table
An isolated PostgreSQL schema (e.g., tenant_acme)
Its own operational tables (sales, products, inventory, etc.)
Users linked through the public.tenant_users join table
All global entities (tenants, SaaS users, plans) live in the public schema, while operational data is isolated per tenant schema.
Creating a New Tenant
Authenticate as Superuser
Only superusers can create new tenants. Obtain a valid JWT token with superuser privileges. curl -X POST https://api.torn.cl/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected] ",
"password": "your-password"
}'
Register the Tenant
Provision a new tenant with company information and economic activities: curl -X POST https://api.torn.cl/saas/tenants \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ACME Corporation",
"rut": "76123456-7",
"address": "Av. Providencia 1234",
"commune": "Providencia",
"city": "Santiago",
"giro": "Comercio al por menor",
"billing_day": 15,
"economic_activities": [
{
"code": "464903",
"name": "Venta al por menor de artículos de ferretería",
"category": "1ra",
"taxable": true
}
]
}'
Response: {
"id" : 5 ,
"name" : "ACME Corporation" ,
"rut" : "76123456-7" ,
"schema_name" : "tenant_acme_76123456" ,
"is_active" : true ,
"plan_id" : 1 ,
"created_at" : "2026-03-08T10:30:00Z"
}
Verify Schema Creation
The provisioning process automatically:
Creates the PostgreSQL schema
Migrates all operational tables (products, sales, cash, etc.)
Inserts default data (roles, payment methods)
Creates an issuer record with the tenant’s fiscal data
The schema_name is auto-generated as tenant_{sanitized_name}_{rut} to ensure uniqueness and readability.
You can update tenant details including DTE (tax document) configuration:
curl -X PATCH https://api.torn.cl/saas/tenants/5 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"address": "Nueva Dirección 456",
"commune": "Las Condes",
"max_users_override": 10,
"economic_activities": [
{
"code": "477310",
"name": "Venta al por menor en comercios no especializados",
"category": "1ra",
"taxable": true
}
]
}'
Updating DTE-related fields (name, address, giro, economic_activities) triggers synchronization with the tenant’s local issuers table.
Managing Tenant Users
Adding a User to a Tenant
Check Plan Limits
Ensure the tenant hasn’t reached their user limit. Limits are enforced based on:
tenant.max_users_override (if set)
Otherwise, plan.max_users
Assign User to Tenant
curl -X POST https://api.torn.cl/saas/tenants/5/users \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected] ",
"full_name": "Juan Pérez",
"password": "secure-password-123",
"role_name": "VENDEDOR"
}'
What happens:
Creates a SaaSUser in public.saas_users (if doesn’t exist)
Links user to tenant via public.tenant_users
Synchronizes user to tenant’s local users table
Assigns the role from the tenant’s local roles table
Verify User Assignment
curl -X GET https://api.torn.cl/saas/tenants/5/users \
-H "Authorization: Bearer YOUR_TOKEN"
Response: [
{
"id" : 12 ,
"tenant_id" : 5 ,
"user_id" : 8 ,
"role_name" : "VENDEDOR" ,
"is_active" : true ,
"user" : {
"email" : "[email protected] " ,
"full_name" : "Juan Pérez"
}
}
]
Updating User Roles and Status
Update a user’s role or deactivate them:
curl -X PATCH https://api.torn.cl/saas/tenants/5/users/8 \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"role_name": "ADMINISTRADOR",
"is_active": true,
"password": "new-password-456"
}'
Source: /app/routers/saas.py:289-370
Changes to user data are automatically synchronized to the tenant’s local schema to maintain consistency.
Searching Economic Activity Codes (ACTECO)
Search for Chilean tax activity codes:
curl -X GET "https://api.torn.cl/saas/actecos?q=ferreteria&limit=30" \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
[
{
"code" : "464903" ,
"name" : "Venta al por menor de artículos de ferretería, pinturas y productos de vidrio"
},
{
"code" : "464904" ,
"name" : "Venta al por menor de materiales de construcción"
}
]
Source: /app/routers/saas.py:20-39
Deactivating a Tenant
Soft-delete a tenant (preserves data):
curl -X DELETE https://api.torn.cl/saas/tenants/5 \
-H "Authorization: Bearer YOUR_TOKEN"
This sets is_active = false but does not drop the schema .
Deactivating a tenant prevents access but retains all historical data. Schema deletion requires manual database intervention.
System User Injection
For support and administrative purposes, inject the Torn system user:
# Single tenant
curl -X POST https://api.torn.cl/saas/tenants/5/inject-system-user \
-H "Authorization: Bearer YOUR_TOKEN"
# All tenants
curl -X POST https://api.torn.cl/saas/tenants/inject-system-users-all \
-H "Authorization: Bearer YOUR_TOKEN"
This creates a special user ([email protected] ) with admin privileges for technical support.
Source: /app/routers/saas.py:373-464
Listing All Tenants
Retrieve all registered tenants:
curl -X GET https://api.torn.cl/saas/tenants \
-H "Authorization: Bearer YOUR_TOKEN"
Response:
[
{
"id" : 1 ,
"name" : "Demo Store" ,
"rut" : "76000001-K" ,
"schema_name" : "tenant_demo_76000001" ,
"is_active" : true ,
"plan_id" : 1
},
{
"id" : 2 ,
"name" : "ACME Corporation" ,
"rut" : "76123456-7" ,
"schema_name" : "tenant_acme_76123456" ,
"is_active" : true ,
"plan_id" : 2
}
]
Key Implementation Details
Tenant Provisioning Service
The provision_new_tenant() function in /app/services/tenant_service.py handles:
Schema name generation and sanitization
Schema creation via raw SQL (CREATE SCHEMA)
Table migration using Alembic metadata
Default data seeding (roles, payment methods, issuer)
Transaction rollback on any failure
Source: Referenced in /app/routers/saas.py:66-84
Global vs Local User Synchronization
Users exist in two places:
Global : public.saas_users (single source of truth for authentication)
Local : {schema}.users (operational context for sales, cash sessions, etc.)
Changes are synchronized using raw SQL with dynamic schema names:
update_issuer_sql = text( f '''
UPDATE " { tenant.schema_name } ".users
SET role = :role, role_id = :role_id
WHERE email = :email
''' )
Source: /app/routers/saas.py:266-282
Best Practices
Security
Always validate superuser status before tenant operations
Use parameterized queries to prevent SQL injection with dynamic schemas
Enforce plan limits at the API layer, not just the database
Data Integrity
Wrap provisioning in transactions with proper rollback
Synchronize global and local user data on every update
Validate economic activity codes against the ACTECO catalog
Performance
Index schema_name and rut columns for fast tenant resolution
Use connection pooling with schema-specific sessions
Limit ACTECO search results (default 30, max 100)