FILE: _proc/00_db_host.html.md # 🏠 Multi-Tenant Setup ## 🎯 Overview The **Host Database** is the backbone of a multi-tenant architecture. It answers the critical questions:
| Question | Model | Purpose |
|---|---|---|
| 👤 Who is this person? | GlobalUser |
Identity & authentication |
| 🏢 Where is their data? | TenantCatalog |
Database routing |
| 🔑 What can they access? | Membership |
Access control |
| 💳 Are they paying? | Subscription |
Billing status |
| 📋 What happened? | HostAuditLog |
Security audit trail |
| ⚙️ Background work? | SystemJob |
Async operations |
| Parameter | Description |
|---|---|
db |
fastsql/minidataapi Database object or HostDatabase instance |
| Function | Description |
|---|---|
timestamp() |
🕐 Returns current UTC time in ISO format |
gen_id() |
🆔 Generates a unique 32-character hex ID |
get_db_uri(db) |
🔗 Extract full connection URI with password from Database object |
| Model | Table Name | Primary Key | Description |
|---|---|---|---|
GlobalUser |
core_users |
id |
OAuth identity, email, optional password hash, Stripe customer ID |
TenantCatalog |
core_tenants |
id |
Maps tenant ID to database URL, tracks plan tier and status |
Membership |
core_memberships |
id |
Links users to tenants with roles (owner,
admin, member) |
Subscription |
core_subscriptions |
id |
Stripe subscription state for billing enforcement |
PricingPlan |
core_pricing_plans |
id |
Available subscription tiers with Stripe price IDs |
HostAuditLog |
sys_audit_logs |
id |
Immutable security log for compliance |
SystemJob |
sys_jobs |
id |
Background task queue for provisioning, cleanup |
StripeWebhookEvent |
core_stripe_webhook_events |
id |
Idempotency tracking for processed Stripe webhook events |
| Method | Description |
|---|---|
from_env() |
🏭 Factory method - creates instance from environment variables |
commit() |
✅ Commit the current database transaction |
rollback() |
↩︎️ Rollback the current transaction on error |
reset_instance() |
🧪 Reset singleton (testing only) |
| Variable | Default | Description |
|---|---|---|
DB_TYPE |
POSTGRESQL |
Database type (POSTGRESQL or SQLITE) |
DB_USER |
postgres |
Database username |
DB_PASS |
(required) | Database password |
DB_HOST |
localhost |
Database host |
DB_PORT |
5432 |
Database port |
DB_NAME |
app_host |
Database name |
| Model | Purpose |
|---|---|
👤 TenantUser |
Local user profiles linked to global identity |
🔐 TenantPermission |
Fine-grained resource permissions |
⚙️ TenantSettings |
Tenant-wide configuration |
| Parameter | Description |
|---|---|
tenant_id |
Unique tenant identifier (from Membership) |
tenant_name |
Optional display name (defaults to tenant_id) |
| Model | Table Name | Primary Key | Description |
|---|---|---|---|
TenantUser |
core_tenant_users |
id |
Links to GlobalUser.id, stores local role &
preferences |
TenantPermission |
core_permissions |
id |
Resource + action permissions (RBAC) |
TenantSettings |
core_settings |
id |
Timezone, currency, feature flags |
| Role | Level | Description |
|---|---|---|
admin |
3 | Full access to all resources and settings |
editor |
2 | Can view and modify data, but not settings |
viewer |
1 | Read-only access to data |
| Field | Description |
|---|---|
resource |
What the permission applies to (e.g., “transactions”, “budgets”) |
action |
What action is allowed (e.g., “view”, “edit”, “delete”) |
granted |
True = allowed, False = explicitly denied |
| Function | Purpose |
|---|---|
register_table |
Create table from dataclass model |
register_tables |
Create multiple tables atomically |
drop_table |
Drop table if exists |
create_index |
Create index with dialect-specific SQL |
drop_index |
Drop index if exists |
list_tables |
List all tables in database |
list_indexes |
List indexes for a table |
| Function | Purpose |
|---|---|
register_table |
Create single table from dataclass |
register_tables |
Create multiple tables atomically |
drop_table |
Drop table if exists |
| Function | Purpose |
|---|---|
create_index |
Create index with dialect-specific SQL |
create_indexes |
Create multiple indexes atomically |
drop_index |
Drop index if exists |
| Parameter | Description |
|---|---|
table_name |
Name of the table |
columns |
List of column names to index |
unique |
If True, creates UNIQUE index (default: False) |
index_name |
Custom name (auto-generates idx_{table}_{cols} if
None) |
| Function | Purpose |
|---|---|
table_exists |
Check if a table exists |
| Parameter | Default | Description |
|---|---|---|
max_age |
3600 |
Session expires after this many seconds of inactivity |
sliding |
True |
Refresh session on each request |
absolute_max |
None |
Optional hard limit regardless of activity (e.g., 86400 for 24h) |
secure |
True |
HTTPS-only cookies in production |
same_site |
"lax" |
SameSite cookie policy for CSRF protection |
| Category | Functions | Purpose |
|---|---|---|
| ⏰ Sliding Sessions | SessionConfig,
SlidingSessionMiddleware,
create_session_middleware |
Sliding session expiry (inactivity timeout) |
| 🛡️ Beforeware | create_auth_beforeware |
Protect routes, auto-setup tenant DB |
| 🔑 OAuth Client | get_google_oauth_client |
Initialize Google OAuth |
| 🔒 CSRF | generate_oauth_state,
verify_oauth_state |
Prevent session hijacking |
| 👤 Users | create_or_get_global_user,
get_user_membership,
verify_membership |
User & membership management |
| 🏗️ Provisioning | provision_new_user |
Auto-create tenant for new users |
| 📋 Session | create_user_session,
get_current_user,
clear_session |
Session management |
| 🚦 Routing | auth_redirect,
route_user_after_login,
require_tenant_access |
Authorization & routing |
| 🌐 Handlers | handle_login_request,
handle_oauth_callback,
handle_logout |
Route implementations |
| Feature | Protection |
|---|---|
| CSRF State Token | Prevents session hijacking attacks |
| Membership Validation | Ensures cross-tenant isolation |
| Audit Logging | Tracks all authentication events |
| Token Type | Expiry | Behavior |
|---|---|---|
| Google OAuth | 1 hour | User must re-login (refresh tokens: future) |
| Session (sliding) | 1 hour inactivity | Refreshes on each request via SlidingSessionMiddleware |
| Role | Level | Automatic For |
|---|---|---|
admin |
3 | Tenant owners |
editor |
2 | — |
viewer |
1 | — |
| Function | Purpose |
|---|---|
has_min_role |
Check if user meets minimum role requirement |
require_role |
Route decorator for role-based protection |
get_user_role |
Derive effective role from session + tenant DB |
| Parameter | Default | Description |
|---|---|---|
session_cache |
False |
Enable caching user dict in session |
session_cache_ttl |
300 |
Cache TTL in seconds (5 minutes) |
| Event | Action |
|---|---|
| Logout | Automatic (session cleared) |
| Role change | Call invalidate_auth_cache(session) |
| TTL expiry | Automatic refresh on next request |
| Function | Purpose |
|---|---|
create_auth_beforeware |
Factory to create route protection middleware |
| Function | Purpose |
|---|---|
get_google_oauth_client |
Initialize Google OAuth client from env vars |
| Function | Purpose |
|---|---|
generate_oauth_state |
Create random UUID for CSRF protection |
verify_oauth_state |
Validate callback state matches session |
| Function | Purpose |
|---|---|
create_or_get_global_user |
Create or retrieve user from host DB |
get_user_membership |
Get user’s active tenant membership |
verify_membership |
Validate user has access to tenant |
| Function | Purpose |
|---|---|
provision_new_user |
Create tenant DB, catalog entry, membership, and TenantUser |
| Function | Purpose |
|---|---|
create_user_session |
Populate session after successful OAuth |
get_current_user |
Extract user info from session |
clear_session |
Clear all session data (logout) |
| Function | Purpose |
|---|---|
auth_redirect |
HTMX-aware redirect to login page |
route_user_after_login |
Determine redirect URL based on user type |
require_tenant_access |
Get tenant DB with membership validation |
| Function | Purpose |
|---|---|
handle_login_request |
Initiate OAuth with CSRF protection |
handle_oauth_callback |
Process provider response |
handle_logout |
Clear session and redirect |
| Function | Purpose |
|---|---|
configure_logging |
One-time setup at app startup |
| Variable | Default | Description |
|---|---|---|
FH_SAAS_LOG_LEVEL |
WARNING | DEBUG, INFO, WARNING, ERROR |
FH_SAAS_LOG_FILE |
(none) | Path to log file (optional) |
| Configuration | Console | File |
|---|---|---|
configure_logging() |
✅ | ❌ |
configure_logging(log_file='app.log') |
✅ | ✅ |
No configure_logging()
call |
❌ silent | ❌ |
| Level | When to Use | Example |
|---|---|---|
DEBUG |
Detailed diagnostic | logger.debug('Checking user session') |
INFO |
Normal operations | logger.info('User logged in') |
WARNING |
Unexpected but not failing | logger.warning('Token expiring soon') |
ERROR |
Operation failed | logger.error('Auth failed', exc_info=True) |
| Category | Functions | Purpose |
|---|---|---|
| 🔄 Client | AsyncAPIClient |
Async HTTP with retry |
| 🔑 Auth | bearer_token_auth,
api_key_auth,
oauth_token_auth |
Auth header generators |
| Method | Purpose |
|---|---|
AsyncAPIClient |
Async HTTP client with retry |
.request() |
Execute HTTP request with retry |
.get_json() |
GET request returning JSON |
| Function | Purpose |
|---|---|
bearer_token_auth |
Bearer token header |
api_key_auth |
API key header (customizable) |
oauth_token_auth |
OAuth 2.0 access token |
| Method | Purpose |
|---|---|
GraphQLClient |
GraphQL client with async generator pagination |
.from_url() |
✨ NEW - Create client from URL with optional bearer token |
.execute() |
✨ NEW - Unified method for queries and mutations |
.execute_query() |
Execute single query (returns full response) |
.execute_mutation() |
Execute mutation (alias for execute_query) |
.fetch_pages_relay() |
✨ NEW - Fetch all pages from Relay-style pagination |
.fetch_pages_generator() |
Stream paginated data (memory-efficient) |
execute_graphql() |
✨ NEW - One-liner function for simple queries |
| Method | Purpose |
|---|---|
.from_url() |
✨ NEW - Create client from URL + token |
.execute() |
✨ NEW - Unified query/mutation (returns data only) |
.execute_query() |
Execute single GraphQL query (full response) |
.execute_mutation() |
Execute GraphQL mutation |
.fetch_pages_relay() |
✨ NEW - Auto-accumulate Relay pagination |
.fetch_pages_generator() |
Async generator for paginated data (streaming) |
| Parameter | Description |
|---|---|
query_template |
GraphQL query with $cursor variable |
variables |
Initial variables dict (e.g., {'cursor': None}) |
items_path |
Path to data list in response (e.g.,
['data', 'users', 'nodes']) |
cursor_path |
Path to next cursor (e.g.,
['data', 'users', 'pageInfo', 'endCursor']) |
has_next_path |
Optional path to hasNextPage flag |
cursor_var |
Cursor variable name in query (default: 'cursor') |
| Function | Purpose |
|---|---|
verify_webhook_signature |
HMAC-SHA256 signature verification |
check_idempotency |
Prevent duplicate processing |
log_webhook_event |
Persist event to database |
process_webhook |
Main orchestration function |
handle_webhook_request |
FastHTML route handler |
| Function | Purpose |
|---|---|
verify_webhook_signature |
HMAC-SHA256 with timing-safe comparison |
| Function | Purpose |
|---|---|
check_idempotency |
Check if event already processed |
| Function | Purpose |
|---|---|
log_webhook_event |
Persist event to database |
update_webhook_status |
Update status after processing |
| Function | Purpose |
|---|---|
process_webhook |
Main orchestration function |
handle_webhook_request |
FastHTML route handler |
| Category | Functions | Purpose |
|---|---|---|
| 🔍 Query Registry | run_id,
validate_params |
Execute queries by ID from centralized registries |
| ➕ Insert-Only | insert_only,
bulk_insert_only |
Insert new records, skip conflicts |
| 🔄 Upsert | upsert,
bulk_upsert |
Insert or update existing records |
| 📝 CRUD | get_by_id,
update_record,
delete_record,
bulk_delete |
Standard database operations |
| 🔧 Utilities | with_transaction,
paginate_sql,
batch_execute |
Transaction management & helpers |
| 💰 Money | to_cents,
from_cents |
Currency conversion for int-only storage |
| Operation | On Conflict | Use Case |
|---|---|---|
insert_only |
Skip (DO NOTHING) | Append-only logs, idempotent imports |
upsert |
Update existing | Sync external data, refresh caches |
| Scenario | Recommended | Why |
|---|---|---|
| Single record | insert_only,
upsert |
Simple, immediate |
| 10+ records | bulk_* variants |
10-100x faster |
| 100+ records | batch_execute |
Commits per batch, memory-safe |
| Multi-table changes | with_transaction |
All-or-nothing commits |
| Function | Purpose |
|---|---|
run_id |
Execute a registered query by its ID |
validate_params |
Ensure all :param placeholders have values |
| Function | Records | SQL Generated |
|---|---|---|
insert_only |
Single | ON CONFLICT DO NOTHING (PG) /
INSERT OR IGNORE (SQLite) |
bulk_insert_only |
Multiple | Same, with batch execution |
| Function | Records | SQL Generated |
|---|---|---|
upsert |
Single | ON CONFLICT DO UPDATE (PG) /
INSERT OR REPLACE (SQLite) |
bulk_upsert |
Multiple | Same, with batch execution |
| Function | Operation | Description |
|---|---|---|
get_by_id |
Read | Fetch single record by primary key |
update_record |
Update | Modify record fields by ID |
delete_record |
Delete | Remove single record by ID |
bulk_delete |
Delete | Remove multiple records by ID list |
| Function | Purpose |
|---|---|
with_transaction |
Context manager for atomic operations |
paginate_sql |
Add LIMIT/OFFSET to queries |
batch_execute |
Process large lists in memory-safe chunks |
| Function | Direction | Example |
|---|---|---|
to_cents |
"$150.00" → 15000 |
Store in DB |
from_cents |
15000 → "$150.00" |
Display to user |
| Category | Components | Purpose |
|---|---|---|
| 📦 Model | TenantJob |
Tenant-level job record with retry support |
| ⚡ Manager | BackgroundTaskManager |
Submit, execute, and track background tasks |
| Field | Type | Description |
|---|---|---|
id |
str | Unique job identifier |
job_type |
str | Job category (e.g., “sync”, “email”) |
status |
str | pending / running / completed
/ failed |
payload |
str | JSON kwargs passed to task function |
result |
str | JSON result on completion |
error_log |
str | Stack trace on failure |
retry_count |
int | Current retry attempt |
max_retries |
int | Max attempts before marking failed |
| Method | Purpose |
|---|---|
submit |
Create job and return BackgroundTask for Starlette |
get_job |
Get job status and details by ID |
list_jobs |
Query jobs with optional type/status filters |
| Function | Purpose |
|---|---|
map_and_upsert |
Bulk JSON→DB upsert via staging table |
apply_schema |
Type conversions (dates, booleans, numbers) |
| Parameter | Description |
|---|---|
df |
Polars DataFrame from JSON |
table_name |
Target database table (must exist) |
key_col |
Primary key for ON CONFLICT resolution |
db_uri |
Database connection string |
column_map |
Optional rename map {json_col: db_col} |
unnest_cols |
Optional list of nested columns to flatten |
type_map |
Optional type casting map {col: pl.DataType} |
| Parameter | Description |
|---|---|
df |
Polars DataFrame |
type_map |
Dict mapping column names to Polars dtypes |
| Type | Handling |
|---|---|
pl.Date |
Parses YYYY-MM-DD strings |
pl.Datetime |
Parses datetime strings |
pl.Boolean |
Converts "true"/"false" strings |
| Other | Uses cast() (works for numeric types) |
| Category | Functions | Purpose |
|---|---|---|
| ⚙️ Configuration | StripeConfig,
StripeConfig.from_env() |
Configure Stripe with env vars or explicit values |
| 💳 Service | StripeService |
Unified API for checkouts, subscriptions, webhooks |
| 🛒 Checkout | create_subscription_checkout,
create_one_time_checkout |
Generate Stripe checkout sessions |
| 🔄 Subscriptions | get_subscription, cancel_subscription,
change_plan |
Manage active subscriptions |
| 🪝 Webhooks | verify_signature, handle_event |
Process Stripe webhook events |
| 🔐 Access Control | get_active_subscription,
has_active_subscription,
require_active_subscription |
Gate features by payment status |
| 📊 Feature Gating | check_feature_access |
Control features by plan tier |
| 🛤️ Route Helpers | create_webhook_route,
create_checkout_route, create_portal_route |
FastHTML route factories |
| Variable | Required | Description |
|---|---|---|
STRIPE_SECRET_KEY |
✅ | Stripe secret API key (sk_live_* or sk_test_*) |
STRIPE_WEBHOOK_SECRET |
✅ | Webhook endpoint signing secret (whsec_*) |
STRIPE_MONTHLY_PRICE_ID |
⚠️ | Pre-created monthly price ID (price_*) |
STRIPE_YEARLY_PRICE_ID |
⚠️ | Pre-created yearly price ID (price_*) |
STRIPE_BASE_URL |
⚠️ | Application base URL for callbacks |
| Category | Functions | Purpose |
|---|---|---|
| ⚙️ Config | get_smtp_config |
Load SMTP settings from env |
| 📁 Templates | get_template_path,
load_template |
Manage markdown templates |
| ✉️ Sending | send_email,
send_batch_emails |
Core send functions |
| 🎁 Convenience | send_welcome_email,
send_invitation_email,
send_password_reset_email |
Pre-built templates |
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_HOST |
✅ | - | SMTP server hostname |
SMTP_PORT |
❌ | 587 | SMTP server port |
SMTP_USER |
✅ | - | Auth username |
SMTP_PASSWORD |
✅ | - | Auth password |
SMTP_MAIL_FROM |
✅ | - | Sender email |
SMTP_STARTTLS |
❌ | True | Use STARTTLS |
SMTP_SSL |
❌ | False | Use SSL (disables TLS) |
| Function | Purpose |
|---|---|
get_smtp_config |
Load SMTP config from environment vars |
| Function | Purpose |
|---|---|
get_template_path |
Resolve path to template file (built-in or custom) |
load_template |
Read template content as string |
| Function | Purpose |
|---|---|
send_email |
Send single email with template |
send_batch_emails |
Send to multiple recipients |
| Function | Purpose |
|---|---|
send_batch_emails |
Send personalized emails to multiple recipients |
| Function | Template | Purpose |
|---|---|---|
send_welcome_email |
welcome.md |
New user onboarding |
send_invitation_email |
invitation.md |
Invite to tenant |
send_password_reset_email |
password_reset.md |
Password recovery |
| Template | Variables |
|---|---|
welcome.md |
{user_name}, {tenant_name},
{dashboard_url}, {to_email} |
invitation.md |
{inviter_name}, {tenant_name},
{invitation_url}, {to_email} |
password_reset.md |
{user_name}, {reset_url},
{to_email} |
| Class | Purpose |
|---|---|
PostLoader |
Load markdown posts from filesystem |
MarkdownEngine |
Render markdown to SEO-friendly HTML |
| Method | Purpose |
|---|---|
.load_posts() |
Load all posts sorted by date |
.get_post() |
Get single post by slug |
| Method | Purpose |
|---|---|
.render() |
Convert markdown to HTML |
This is bold.
``` ------------------------------------------------------------------------ source ### MarkdownEngine.get_toc ``` python def get_toc( )->str: ``` *Get table of contents HTML from last render.* Must call render() first. Returns empty string if no headings. Example: ```python engine = MarkdownEngine() html = engine.render('# Title ## Section 1 ## Section 2’) toc = engine.get_toc() print(toc) #| Function | Purpose |
|---|---|
generate_head_tags |
Meta tags for social sharing |
generate_sitemap_xml |
XML sitemap for crawlers |
generate_json_ld |
Structured data (Article schema) |
| Function | Purpose |
|---|---|
generate_head_tags |
OpenGraph, Twitter Card, canonical |
| Function | Purpose |
|---|---|
generate_sitemap_xml |
XML sitemap for search crawlers |
| Class | Purpose |
|---|---|
Workflow |
Execute callable steps sequentially |
| Method | Purpose |
|---|---|
Workflow(steps) |
Initialize with list of callables |
.execute() |
Run all steps sequentially |