Managing tenants
The tenants list lives at https://booking.netwit.ca/admin/tenants. It is the page you will spend the most time on as a super admin: every business on BookFlow is a tenant, and any time you need to help, investigate, suspend, or modify a tenant, you start here.
What the list shows
One row per tenant, sorted by created_at DESC (newest first). The columns are:
| Column | Description |
|---|---|
| Business | Business name + slug. A red SUSPENDED pill appears if is_suspended=1. |
| Plan | free, pro, or enterprise. Free is the default for new signups. |
| Users | Count of users in tenant_members (owners + staff). |
| Bookings | Count of bookings for this tenant, all-time. |
| Created | Relative time (e.g. '3d ago'). |
| Action | Open the tenant detail page. |
Search and filter
The page has two controls above the table:
- Search box — does a
LIKE '%query%'match againstbusiness_name,slug, andemail. Case-insensitive. No special-character escaping; queries that contain SQL wildcards (%or_) are passed through as literal text in the current build, which is fine for typical use but be careful if your search term is itself a wildcard. - Status filter — a dropdown with three values:
all(default) — every tenant, suspended or notactive— only tenants withis_suspended = 0suspended— only tenants withis_suspended = 1
Note: the API's Zod schema accepts a fourth value trial but the console UI does not expose it. trial is accepted by the schema but currently falls through to the same behavior as all — the WHERE clauses only filter for active / suspended, so a trial value returns every tenant.
Sorting
The API sorts by created_at DESC only. There is no client-side sort control and no other orderBy parameter on GET /api/admin/tenants in the current build. If you need a different sort (e.g. by bookings or revenue), the workaround is to fetch the full result set from the API and sort it in a spreadsheet.
created_at, bookings, and revenue. Only created_at is implemented. Tracking issue: ADMIN-142. Plan to add an orderBy param in a future iteration.Pagination
20 tenants per page. The console shows Prev / Next buttons when there are more than 20 results. The API itself accepts page (min 1, default 1) and limit (min 1, max 100, default 20).
Per-row actions
Clicking a row's Open link takes you to that tenant's detail page, where you can take the actual actions:
- View detail — see the full record, members, services, staff, customers, bookings, payments, AI activity, and the tenant-scoped audit log.
- Suspend — flips
is_suspended = 1, setssuspended_atto now, requires asuspendedReason(free-text, max 500 chars). The tenant's public booking page immediately returns 503. The owner gets an email. - Restore — flips
is_suspended = 0, clearssuspended_at. Public booking page comes back online. - Change plan — set
plantofree,pro, orenterprise. Read the Plans and billing guide for what each plan unlocks. - Impersonate — opens a read-only session as the tenant owner. Every action you take while impersonating is logged to
audit_logwithaction = 'admin.tenant.impersonate'and a reason. The console prompts for a reason before opening the impersonation session.
Bulk actions
The spec called for bulk actions (send email, export). The current console does not yet expose checkboxes for multi-select. If you need to email or export more than a handful of tenants today, use the API and a small script.
# Export all Pro tenants as JSON
curl "https://booking-api.netwit.ca/api/admin/tenants?search=pro&limit=100" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
> pro-tenants.jsonAPI
/api/admin/tenantsList tenants with optional search, status filter, pagination. Returns rows plus a pagination object.
Query parameters
statusenum: all | active | suspended | trialDefault all. active filters to is_suspended = 0; suspended to is_suspended = 1; trial is not exposed in the current console.
searchstringFree-text. Matches against business_name, slug, and email with LIKE %term%.
pageintegerMin 1, default 1.
limitintegerMin 1, max 100, default 20.
Sample call
curl "https://booking-api.netwit.ca/api/admin/tenants?status=active&search=salon&page=1&limit=20" \
-H "Authorization: Bearer $ADMIN_TOKEN"Response shape
{
"data": [
{
"id": "abc123…",
"slug": "demo-salon",
"businessName": "Demo Salon",
"businessType": "salon",
"email": "owner@demosalon.com",
"plan": "pro",
"planStatus": "active",
"isActive": true,
"isSuspended": false,
"suspendedReason": null,
"createdAt": "2026-06-12T18:34:00Z",
"customDomain": null,
"userCount": 3,
"bookingCount": 412
}
],
"pagination": { "page": 1, "limit": 20, "total": 47 }
}isActive reflects the soft-delete flag (is_active in D1). isSuspended is a separate axis — a tenant can be isActive: true, isSuspended: true (soft-deleted but still flagged as suspended in the audit trail) or isActive: true, isSuspended: false (the normal case). The console treats isSuspended = true as the visible state.What this page does not do
- It does not let you edit a tenant's profile, slug, or email. Those are tenant-owned and only the tenant owner can change them.
- It does not let you delete a tenant. Tenants are never hard-deleted from the console. Use a D1 migration to scrub a tenant's data only after legal review.
- It does not show revenue per tenant. Revenue is computed only at the platform level in
GET /api/admin/stats.
Email hello@netwit.ca or call +1-604-206-8169. NetWit responds in 1 business day.