|
| 1 | +# django-fqq |
| 2 | + |
| 3 | +Lightweight PostgreSQL schema-based multi-tenancy for Django. |
| 4 | + |
| 5 | +django-fqq automatically qualifies table names with the appropriate PostgreSQL schema, routing queries to tenant-specific schemas using a simple `ContextVar`-based approach. Shared tables (like `auth`) stay in `public`, while tenant tables are directed to the active schema. |
| 6 | + |
| 7 | +## How it works |
| 8 | + |
| 9 | +django-fqq provides a custom database backend that wraps Django's built-in PostgreSQL backend. It overrides `quote_name` to prepend the active schema to table names — so `"my_table"` becomes `"tenant_schema"."my_table"` automatically. No changes to your models or queries required. |
| 10 | + |
| 11 | +django-fqq does not modify queries if `set_schema` has not been called or has been set to a falsy value (e.g. None or ""). |
| 12 | + |
| 13 | +## Installation |
| 14 | + |
| 15 | +```bash |
| 16 | +pip install django-fqq |
| 17 | +``` |
| 18 | + |
| 19 | +## Configuration |
| 20 | + |
| 21 | +### 1. Set the database backend |
| 22 | + |
| 23 | +```python |
| 24 | +DATABASES = { |
| 25 | + "default": { |
| 26 | + "ENGINE": "django_fqq.backend", |
| 27 | + "NAME": "your_db", |
| 28 | + # ... other options |
| 29 | + } |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +### 2. Add the app |
| 34 | + |
| 35 | +```python |
| 36 | +INSTALLED_APPS = [ |
| 37 | + # ... |
| 38 | + "django_fqq", |
| 39 | +] |
| 40 | +``` |
| 41 | + |
| 42 | +### 3. Configure shared apps |
| 43 | + |
| 44 | +Apps listed in `FQQ_SHARED_APPS` will always use the `public` schema. Everything else uses the active tenant schema. |
| 45 | + |
| 46 | +```python |
| 47 | +FQQ_SHARED_APPS = ["auth", "contenttypes"] |
| 48 | +``` |
| 49 | + |
| 50 | +### 4. Add the middleware (optional) |
| 51 | + |
| 52 | +```python |
| 53 | +MIDDLEWARE = [ |
| 54 | + "django_fqq.middleware.SchemaMiddleware", |
| 55 | + # ... |
| 56 | +] |
| 57 | +``` |
| 58 | + |
| 59 | +Subclass `SchemaMiddleware` and override `_get_schema(request)` to resolve the tenant schema from the request (e.g. from subdomain, header, or URL). |
| 60 | + |
| 61 | +## Usage |
| 62 | + |
| 63 | +### Setting the schema manually |
| 64 | + |
| 65 | +```python |
| 66 | +from django_fqq.schema import set_schema, clear_schema |
| 67 | + |
| 68 | +set_schema("tenant_abc") |
| 69 | +# All queries now target the "tenant_abc" schema |
| 70 | +# ... |
| 71 | +clear_schema() |
| 72 | +``` |
| 73 | + |
| 74 | +### In a middleware or view |
| 75 | + |
| 76 | +```python |
| 77 | +from django_fqq.middleware import SchemaMiddleware |
| 78 | + |
| 79 | +class MySchemaMiddleware(SchemaMiddleware): |
| 80 | + def _get_schema(self, request): |
| 81 | + # Resolve tenant from subdomain |
| 82 | + host = request.get_host().split(".")[0] |
| 83 | + return host |
| 84 | +``` |
| 85 | + |
| 86 | +### Using a context manager |
| 87 | + |
| 88 | +`query_schema` is a context manager that sets the schema for the duration of a block and automatically restores the previous state on exit. If a schema was already active, it is restored rather than cleared. |
| 89 | + |
| 90 | +```python |
| 91 | +from django_fqq.schema import query_schema |
| 92 | + |
| 93 | +with query_schema("tenant_abc"): |
| 94 | + # All queries target "tenant_abc" |
| 95 | + ... |
| 96 | +# Schema is restored to its previous value (or cleared if none was set) |
| 97 | +``` |
| 98 | + |
| 99 | +It supports nesting — each level restores the schema that was active before it: |
| 100 | + |
| 101 | +```python |
| 102 | +from django_fqq.schema import set_schema, query_schema |
| 103 | + |
| 104 | +set_schema("tenant_a") |
| 105 | + |
| 106 | +with query_schema("tenant_b"): |
| 107 | + # queries target "tenant_b" |
| 108 | + with query_schema("tenant_c"): |
| 109 | + # queries target "tenant_c" |
| 110 | + ... |
| 111 | + # back to "tenant_b" |
| 112 | + |
| 113 | +# back to "tenant_a" |
| 114 | +``` |
| 115 | + |
| 116 | +The previous schema is also restored if an exception is raised inside the block. |
| 117 | + |
| 118 | +### Context-safe |
| 119 | + |
| 120 | +Schema state is stored in a `ContextVar`, so it's safe across concurrent async requests and threads. |
| 121 | + |
| 122 | +## Development |
| 123 | + |
| 124 | +Requires Python 3.12+ and Django 6.0+. |
| 125 | + |
| 126 | +```bash |
| 127 | +uv sync |
| 128 | +uv run pytest |
| 129 | +``` |
| 130 | + |
| 131 | +## License |
| 132 | + |
| 133 | +MIT |
0 commit comments