Platform & Tenant Administration (PLT)
Module code PLT · Release R0 (registry, provisioning, plans, flags), R1 (billing, statutory packs), R2 (dedicated-cell automation starts), R4 (full automation) · Packaging Core, operated by Spyrl staff · Requirements
FR-PLT-*,BR-PLT-*in the PRD and BRD · Architecture architecture sections 3, 4, 9, 14 and 15
1. Overview and scope
PLT is the control plane: a separate NestJS service with its own PostgreSQL database that is the system of record for who the tenants are, what they bought and where they run. Tenant users never see it directly. Spyrl staff use it through the platform admin console. The application plane calls it (through a cached SDK) to resolve tenants, check entitlements and evaluate feature flags.
In scope: tenant registry and lifecycle; self-serve trial sign-up; idempotent provisioning; cell registry and placement; plans, add-ons, entitlements and overrides; feature flags; metering and Razorpay billing; version rollout waves; statutory reference packs; break-glass support access; tenant export and deletion; health scores.
Out of scope: anything inside a tenant's HR data (owned by the app-plane modules).
2. Learned from the reference product
Source: Spryple analysis, §3.3, §20 and §21.1.
- [Observed] Every login asks for a Company Code, on web and in all apps (SP-X-10).
- [Inferred] This, plus hash-routed URLs on shared raw IPs in screenshots, suggests a single shared multi-tenant deployment with no per-tenant addressing (§21.1).
- [Observed] Spryple's "Super Admin" is a tenant role (the company owner), not a platform operator. No platform console is documented (§3.2).
- [Observed] A self-serve trial exists at a separate sign-up URL (§21.1).
- [Observed] An implementation team supplies templates (e.g. the attendance Excel) during onboarding (§3.2), so onboarding is partly vendor-driven.
- [Inferred] Pricing tiers and per-module licensing could not be analysed from public docs (§20, question 7).
3. Our design decisions
| ID | Decision [Proposed] | Reason |
|---|---|---|
| D-PLT-01 | Separate control-plane service and database. | Tenancy, billing and rollout must survive any single cell's failure; least privilege. |
| D-PLT-02 | Hybrid cells: pooled by default, dedicated on demand; same images and migrations. | Low cost for SMEs, isolation for enterprise, one release train. |
| D-PLT-03 | Tenants are addressed by an immutable slug (acme.spyrl.app) plus a human-friendly company code for mobile. | Clear routing; the company code is familiar to reference-product users. |
| D-PLT-04 | Entitlements (plan + add-ons + overrides) are separate from feature flags (rollout and kill switches). | Commercial rules and engineering rollout change for different reasons. |
| D-PLT-05 | Statutory rates are published centrally as versioned statutory packs with effective dates; tenants inherit them or accept them explicitly. | One team keeps EPF, ESI, PT, LWF and TDS tables correct for every tenant. |
| D-PLT-06 | Support access is break-glass only: time-boxed, reasoned, consented where the plan requires it, read-only by default and visible to the tenant. | Trust and DPDP accountability. |
| D-PLT-07 | Billing never blocks payroll processing inside the payroll window without senior approval. | A missed salary is worse than a late invoice. |
4. Domain model
Control-plane tables have no module prefix.
5. State machines
5.1 Tenant lifecycle
Suspended is read-only for users; payroll exports remain available. Churned keeps a data-export window (30 or 90 days per contract).
5.2 Support access grant
6. Key business logic
6.1 Provisioning (idempotent steps)
provision(tenantRequest):
step 1 validate slug and company code (reserved words, uniqueness)
step 2 choose cell = placeCell(plan, region)
step 3 insert tenant row in the control plane (status Provisioning)
step 4 in the cell: create tenant root rows, seed reference data
(roles from templates, leave-type templates, components and a default
structure, holiday template, statutory pack acceptance = INHERIT)
step 5 create owner user and send an invite link (72 h, single use)
step 6 register domain or confirm wildcard; custom domain stays Pending
step 7 mark Trial, start metering, emit plt.tenant.provisioned
each step records completion in provisioning_job.step_no; a retry resumes there
6.2 Cell placement
placeCell(plan, region):
if plan requires a dedicated cell: create one (Terraform module) or use the contracted cell
candidates = pooled cells in region with status Open
exclude cells over any threshold: tenants > 300, active employees > 30000,
DB CPU at monthly peak > 60%, DB size > 500 GB
return the candidate with the lowest active_employee_count
if none: raise capacity alert and open a new pooled cell
6.3 Entitlement check (app plane SDK)
entitled(tenant, feature_key):
e = cache.get(tenant) # TTL 60 s, invalidated by plt.entitlements.changed
value = override(feature_key) or addon(feature_key) or plan(feature_key)
return value # boolean or a numeric limit
limit check for employees:
warn at 100% of employee_limit, soft block new hires at 105%, never block payroll runs
6.4 PEPM billing
billable_employees(month) = max over days in month of usage_daily(metric = ACTIVE_EMPLOYEES)
amount = max(plan.min_monthly_fee, billable_employees * plan.pepm_price)
+ sum(addon quantity * addon price) + metered usage (SMS, WhatsApp, tracked users, storage)
GST is added on the invoice.
7. Configuration
| Setting | Type | Default | Description |
|---|---|---|---|
trial.days | integer | 14 | Trial length (14 to 30) |
trial.sample_data | boolean | true | Seed a demo company on sign-up |
billing.grace_days | integer | 7 | Past-due grace before suspension |
billing.export_window_days | integer | 30 | Days after churn to export data |
entitlement.soft_limit_percent | integer | 105 | Employee count before new hires are blocked |
support.consent_required | boolean | plan-based | Enterprise and Professional require owner consent |
support.max_grant_hours | integer | 8 | Maximum duration of a grant |
rollout.wave_soak_hours | array of integer | [24, 48] | SLO-green time before the next wave |
statutory_pack.mode | enum | INHERIT | INHERIT or ACCEPT_EXPLICITLY per tenant |
cell.thresholds | object | see 6.2 | Capacity limits for pooled cells |
8. Screens by persona
| Persona | Screens |
|---|---|
platform_admin | Tenant list and detail (plan, usage, cell, health score), suspend and reactivate, trial extension, export and deletion, cells and capacity, feature flags, statutory pack publishing |
billing_admin | Plans and prices, subscriptions, invoices, dunning queue, credit notes |
release_manager | Cell versions, rollout waves, maintenance windows, release notes |
support_engineer | Request access, active grants, tenant-visible access log |
org_owner (tenant side) | Subscription page (plan, usage, invoices, add-ons), support-access consent prompt and log, data export request |
| Prospect | Self-serve sign-up (company, owner, slug, plan, mobile OTP) |
9. Events
| Event | Consumers |
|---|---|
plt.tenant.provisioned | App cell seeding confirmation, NTF (welcome), CRM of Spyrl sales |
plt.tenant.activated | Billing, NTF |
plt.tenant.suspended | App plane (read-only mode), NTF |
plt.tenant.reactivated | App plane |
plt.tenant.churned | Export job, NTF |
plt.tenant.deleted | Cell purge job, deletion certificate |
plt.entitlements.changed | App-plane SDK cache invalidation, MOB navigation refresh |
plt.flag.changed | SDK cache invalidation |
plt.statutory_pack.published | PAY (new rate versions), NTF (payroll managers) |
plt.support_access.granted | IAM (temporary session), tenant audit log |
plt.cell.version_changed | Release notes, monitoring |
Consumed from cells: daily usage snapshots (emp.employee.joined, ext.lwd.reached aggregates), pay.run.locked (health score), error-rate metrics.
10. API surface
Control-plane APIs are internal; public resolution is the only anonymous endpoint. Detailed in API endpoints.
| Resource | Main operations |
|---|---|
/public/tenants/resolve?code= | GET company code to API base URL and branding (mobile) |
/public/signup | POST self-serve trial |
/cp/v1/tenants | CRUD, POST /:id/suspend, /:id/reactivate, /:id/export, /:id/delete |
/cp/v1/cells | CRUD, capacity, versions |
/cp/v1/plans, /cp/v1/tenants/:id/addons, /cp/v1/tenants/:id/overrides | CRUD |
/cp/v1/flags | CRUD, rules |
/cp/v1/statutory-packs | POST draft, POST /:id/publish |
/cp/v1/support-access | POST request, POST /:id/consent, POST /:id/revoke |
/v1/plt/subscription (tenant side) | GET plan, usage, invoices; POST change plan or add-ons |
11. Permissions
Platform roles are separate from tenant roles and use the control-plane identity provider with mandatory MFA.
| Permission | Roles |
|---|---|
plt.tenant.read, plt.tenant.manage | platform_admin |
plt.tenant.suspend | platform_admin, billing_admin |
plt.billing.manage | billing_admin |
plt.cell.manage, plt.rollout.manage | release_manager, platform_admin |
plt.flag.manage | platform_admin, release_manager |
plt.statutory_pack.publish | platform_admin (two-person review) |
plt.support_access.request | support_engineer |
plt.support_access.consent@ORG | org_owner (tenant side) |
plt.subscription.manage@ORG | org_owner (tenant side) |
12. Reports
| Report | Contents |
|---|---|
| Tenant health | Setup completion (go-live readiness), DAU/MAU, punches per day, last payroll run status, open tickets |
| Revenue and usage | MRR, PEPM by plan, add-on attach rate, metered usage |
| Trial funnel | Sign-ups, activated, converted, by source |
| Cell capacity | Tenants, employees, DB CPU and size, p95 latency per cell |
| Support access log | Every grant with reason and actions (also visible to the tenant) |
13. Release and priority
| Capability | Release | MoSCoW |
|---|---|---|
| Tenant registry, slug and company code, provisioning, one pooled cell | R0 | Must |
| Plans, entitlements SDK, feature flags, admin console v1 | R0 | Must |
| Support access with audit | R0 | Must |
| Razorpay billing, metering, dunning, GST invoices | R1 | Must |
| Statutory packs with publishing and acceptance | R1 | Must |
| Tenant export and deletion certificate | R1 | Must |
| Custom domains | R2 | Should |
| Dedicated-cell provisioning via Terraform | R2 | Should |
| Wave rollout orchestrator | R2 | Should |
| Health scores and customer-success views | R2 | Could |
| Cell mobility tool (move a tenant between cells) | R4 | Should |
14. Open questions
- Final plan prices and the Lite plan limits need commercial validation (ADR-013 is Proposed).
- Do we let channel partners (CA firms, payroll bureaus) manage several tenants from one login? This would need a partner role in PLT.
- Should statutory packs be accepted per legal entity or per tenant? The default is per tenant.
- How long do we keep a deleted tenant's backups (they age out of PITR and snapshots)? This must be stated in the deletion certificate.