Skip to main content
One idea at a time

Entitlements are a server decision

An active subscription is provider state. A workspace's ability to create more projects is an entitlement: an application decision derived from provider state and Taskboard's product rules.

Keeping those concepts separate makes billing behavior explainable. Stripe can report several subscription states. Taskboard needs an explicit policy for which states allow the paid plan and what happens when a subscription becomes overdue or ends.

Store the local interpretation​

The local subscription record connects a workspace to provider customer and subscription identifiers. Reconciliation updates the provider status and the plan information the server uses for authorization.

Illustrative entitlement policy
Provider status: active
Local entitlement: paid workspace limits

Provider status: canceled
Local entitlement: free workspace limits

Taskboard stores team when the canonical subscription is active or trialing and its price matches the configured price. Other states map to free. There is no payment grace period. During a provider outage, the last confirmed team plan stays effective for seventy-two hours after its successful sync. After that freshness window, quota checks use free limits until reconciliation recovers.

From the working appRead api/src/tenancy.ts
export function effectivePlan(workspace: Pick<typeof workspaces.$inferSelect, 'plan' | 'billingSyncedAt'>): 'free' | 'team' {
return workspace.plan === 'team' && workspace.billingSyncedAt && Date.now() - workspace.billingSyncedAt.getTime() <= billingFreshnessWindowMs ? 'team' : 'free';
}

The default member limits are three on free and twenty-five on team. Project creation permits three free projects or one hundred team projects. The billing response exposes the effective plan and last sync time so the client can explain stale state.

The frontend can hide an unavailable button or show an upgrade prompt. The server must still enforce the limit because a caller can send HTTP requests without the interface.

A limit is a concurrency problem​

Suppose a workspace has room for one more project. Two admins submit creation requests simultaneously. Both can read the same project count before either inserts. Checking a count outside coordination can let both succeed.

Taskboard needs a transaction and a shared lock or another database guarantee around checking the limit and making the change. A workspace row is a useful coordination point when all competing operations belong to that workspace. The resulting observation is one allowed creation and one limit response, rather than two accepted creations.

A downgrade raises another product question. Existing data may exceed the free limit. Automatically deleting projects would surprise users and destroy data. A documented policy can preserve existing projects while blocking further creation. Billing correctness includes that user-visible behavior.

Inspect subscription translation in Read api/src/billing.ts, member limits in Read api/src/tenancy.ts, and project limits in Read api/src/tasks.ts.

Take away

Subscription status informs entitlement, but the app defines entitlement policy. Enforce limits in coordinated server-side operations.

Why is a project-count check before the transaction insufficient?

Another request can create a project after the count is read. The check and insert need a shared concurrency rule so they act on a protected decision.