Skip to content

Commit bc6347e

Browse files
authored
[Feat]: set flag to disable billing (#1417)
### Context There are some kinks to work out with deploying plan limits onto prod, so we'd like to disable it temporarily. ### Summary of Changes We update all call sites of the item quantity things with a flag based check. Idea is when flag is set to true, it should function as if there are no limits.
1 parent bd8c448 commit bc6347e

13 files changed

Lines changed: 83 additions & 15 deletions

File tree

apps/backend/.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ STACK_SPOTIFY_CLIENT_SECRET=# client secret
3434

3535
STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=# allow shared oauth provider to also use connected account access token, this should only be used for development and testing
3636

37+
STACK_DISABLE_PLAN_LIMITS=# set to "true" to bypass enforcement of Stack Auth's own internal-tenancy plan limits (analytics_events, session_replays, emails_per_month, dashboard_admins seat cap, auth_users soft cap, analytics_timeout_seconds). Default unset/false preserves enforcement. Intended as a temporary cutover safety net while the plan-limits infrastructure rolls out — customer projects' own item APIs are unaffected by this flag.
38+
3739
# Email
3840
# For local development, you can spin up a local SMTP server like inbucket
3941
STACK_EMAIL_HOST=# for local inbucket: 127.0.0.1

apps/backend/.env.development

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ STACK_SPOTIFY_CLIENT_SECRET=MOCK
4343

4444
STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS=true
4545

46+
# Default to enforcing plan limits in local dev so behavior matches prod.
47+
# Flip to "true" to bypass every Stack-Auth-internal plan-limit enforcement
48+
# site (e.g. session_replays, analytics_events, emails_per_month). See
49+
# apps/backend/src/lib/plan-entitlements.ts:arePlanLimitsEnforced.
50+
STACK_DISABLE_PLAN_LIMITS=false
51+
4652
STACK_DATABASE_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28/stackframe
4753
STACK_DATABASE_REPLICA_CONNECTION_STRING=postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}34/stackframe
4854
STACK_DATABASE_REPLICATION_WAIT_STRATEGY=pg-stat-replication

apps/backend/src/app/api/latest/analytics/events/batch/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getClickhouseAdminClient } from "@/lib/clickhouse";
2-
import { getBillingTeamId } from "@/lib/plan-entitlements";
2+
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
33
import { findRecentSessionReplay } from "@/lib/session-replays";
44
import { getStackServerApp } from "@/stack";
55
import { getPrismaClientForTenancy } from "@/prisma-client";
@@ -121,7 +121,7 @@ export const POST = createSmartRouteHandler({
121121
const app = getStackServerApp();
122122

123123
const billingTeamId = getBillingTeamId(auth.tenancy.project);
124-
if (billingTeamId != null) {
124+
if (billingTeamId != null && arePlanLimitsEnforced()) {
125125
const eventsItem = await app.getItem({ itemId: ITEM_IDS.analyticsEvents, teamId: billingTeamId });
126126
const isDebited = await eventsItem.tryDecreaseQuantity(body.events.length);
127127
if (!isDebited) {

apps/backend/src/app/api/latest/internal/analytics/query/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getClickhouseExternalClient } from "@/lib/clickhouse";
22
import { getSafeClickhouseErrorMessage } from "@/lib/clickhouse-errors";
3-
import { getBillingTeamId } from "@/lib/plan-entitlements";
3+
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
44
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
55
import { getStackServerApp } from "@/stack";
66
import { KnownErrors } from "@stackframe/stack-shared";
@@ -42,7 +42,7 @@ export const POST = createSmartRouteHandler({
4242

4343
let effectiveTimeoutMs = body.timeout_ms;
4444
const billingTeamId = getBillingTeamId(auth.tenancy.project);
45-
if (billingTeamId != null) {
45+
if (billingTeamId != null && arePlanLimitsEnforced()) {
4646
const app = getStackServerApp();
4747
const timeoutItem = await app.getItem({ itemId: ITEM_IDS.analyticsTimeoutSeconds, teamId: billingTeamId });
4848
// clickHouse treats max_execution_time=0 as

apps/backend/src/app/api/latest/internal/send-test-email/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { isSecureEmailPort, lowLevelSendEmailDirectWithoutRetries } from "@/lib/emails-low-level";
2-
import { getBillingTeamId } from "@/lib/plan-entitlements";
2+
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
33
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
44
import { getStackServerApp } from "@/stack";
55
import { KnownErrors } from "@stackframe/stack-shared";
@@ -49,7 +49,7 @@ export const POST = createSmartRouteHandler({
4949
// The debit is refunded on any failure below so admins iterating on an
5050
// incorrect SMTP config don't burn through their monthly quota.
5151
const billingTeamId = getBillingTeamId(auth.tenancy.project);
52-
const emailItem = billingTeamId == null
52+
const emailItem = billingTeamId == null || !arePlanLimitsEnforced()
5353
? null
5454
: await getStackServerApp().getItem({ itemId: ITEM_IDS.emailsPerMonth, teamId: billingTeamId });
5555
if (emailItem != null && billingTeamId != null) {

apps/backend/src/app/api/latest/session-replays/batch/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getPrismaClientForTenancy } from "@/prisma-client";
22
import { uploadBytes } from "@/s3";
33
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
44
import { Prisma } from "@/generated/prisma/client";
5-
import { getBillingTeamId } from "@/lib/plan-entitlements";
5+
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
66
import { findRecentSessionReplay } from "@/lib/session-replays";
77
import { getStackServerApp } from "@/stack";
88
import { KnownErrors } from "@stackframe/stack-shared";
@@ -113,7 +113,7 @@ export const POST = createSmartRouteHandler({
113113

114114
const isNewSession = recentSession == null;
115115
const billingTeamId = getBillingTeamId(auth.tenancy.project);
116-
if (isNewSession && billingTeamId != null) {
116+
if (isNewSession && billingTeamId != null && arePlanLimitsEnforced()) {
117117
const replaysItem = await app.getItem({ itemId: ITEM_IDS.sessionReplays, teamId: billingTeamId });
118118
const isDebited = await replaysItem.tryDecreaseQuantity(1);
119119
if (!isDebited) {

apps/backend/src/app/api/latest/team-invitations/[id]/accept/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { teamMembershipsCrudHandlers } from "@/app/api/latest/team-memberships/crud";
22
import { getItemQuantityForCustomer } from "@/lib/payments/customer-data";
3+
import { arePlanLimitsEnforced } from "@/lib/plan-entitlements";
34
import { getPrismaClientForTenancy, retryTransaction } from "@/prisma-client";
45
import { globalPrismaClient } from "@/prisma-client";
56
import { VerificationCodeType } from "@/generated/prisma/client";
@@ -104,7 +105,7 @@ export const POST = createSmartRouteHandler({
104105
}
105106

106107
await retryTransaction(prisma, async (tx) => {
107-
if (auth.tenancy.project.id === "internal") {
108+
if (auth.tenancy.project.id === "internal" && arePlanLimitsEnforced()) {
108109
const currentMemberCount = await tx.teamMember.count({
109110
where: {
110111
tenancyId: auth.tenancy.id,

apps/backend/src/app/api/latest/team-invitations/accept/verification-code-handler.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { teamMembershipsCrudHandlers } from "@/app/api/latest/team-memberships/crud";
22
import { normalizeEmail, sendEmailFromDefaultTemplate } from "@/lib/emails";
33
import { getItemQuantityForCustomer } from "@/lib/payments/customer-data";
4+
import { arePlanLimitsEnforced } from "@/lib/plan-entitlements";
45
import { getSoleTenancyFromProjectBranch } from "@/lib/tenancies";
56
import { getPrismaClientForTenancy } from "@/prisma-client";
67
import { createVerificationCodeHandler } from "@/route-handlers/verification-code-handler";
@@ -102,7 +103,7 @@ export const teamInvitationCodeHandler = createVerificationCodeHandler({
102103
}
103104
const prisma = await getPrismaClientForTenancy(tenancy);
104105

105-
if (tenancy.project.id === "internal") {
106+
if (tenancy.project.id === "internal" && arePlanLimitsEnforced()) {
106107
const currentMemberCount = await prisma.teamMember.count({
107108
where: {
108109
tenancyId: tenancy.id,

apps/backend/src/app/api/latest/users/crud.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { BooleanTrue, Prisma } from "@/generated/prisma/client";
22
import { getRenderedOrganizationConfigQuery, getRenderedProjectConfigQuery } from "@/lib/config";
33
import { demoteAllContactChannelsToNonPrimary, setContactChannelAsPrimaryByValue } from "@/lib/contact-channel";
44
import { normalizeEmail } from "@/lib/emails";
5-
import { getBillingTeamId, getTeamWideAuthUsersCapacity, getTeamWideNonAnonymousUserCount } from "@/lib/plan-entitlements";
5+
import { arePlanLimitsEnforced, getBillingTeamId, getTeamWideAuthUsersCapacity, getTeamWideNonAnonymousUserCount } from "@/lib/plan-entitlements";
66
import { recordExternalDbSyncContactChannelDeletionsForUser, recordExternalDbSyncDeletion, recordExternalDbSyncNotificationPreferenceDeletionsForUser, recordExternalDbSyncOAuthAccountDeletionsForUser, recordExternalDbSyncProjectPermissionDeletionsForUser, recordExternalDbSyncRefreshTokenDeletionsForUser, recordExternalDbSyncTeamMemberDeletionsForUser, recordExternalDbSyncTeamPermissionDeletionsForUser, withExternalDbSyncUpdate } from "@/lib/external-db-sync";
77
import { grantDefaultProjectPermissions } from "@/lib/permissions";
88
import { ensureTeamMembershipExists, ensureUserExists } from "@/lib/request-checks";
@@ -268,6 +268,9 @@ async function checkAuthUsersSoftLimit(tenancy: Tenancy) {
268268
if (getEnvVariable('STACK_SEED_MODE', '') === 'true') {
269269
return;
270270
}
271+
if (!arePlanLimitsEnforced()) {
272+
return;
273+
}
271274
const billingTeamId = getBillingTeamId(tenancy.project);
272275
if (billingTeamId == null) {
273276
return;

apps/backend/src/lib/email-queue-step.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { calculateCapacityRate, getEmailCapacityBoostExpiresAt, getEmailDelivery
33
import { getEmailThemeForThemeId, renderEmailsForTenancyBatched } from "@/lib/email-rendering";
44
import { EmailOutboxRecipient, getEmailConfig, } from "@/lib/emails";
55
import { generateUnsubscribeLink, getNotificationCategoryById, hasNotificationEnabled, listNotificationCategories } from "@/lib/notification-categories";
6-
import { getBillingTeamId } from "@/lib/plan-entitlements";
6+
import { arePlanLimitsEnforced, getBillingTeamId } from "@/lib/plan-entitlements";
77
import { getStackServerApp } from "@/stack";
88
import { ITEM_IDS } from "@stackframe/stack-shared/dist/plans";
99
import { getTenancy, Tenancy } from "@/lib/tenancies";
@@ -693,7 +693,7 @@ async function processSingleEmail(context: TenancyProcessingContext, row: EmailO
693693
}
694694
}
695695

696-
if (context.billingTeamId != null && row.sendRetries === 0) {
696+
if (context.billingTeamId != null && row.sendRetries === 0 && arePlanLimitsEnforced()) {
697697
const app = getStackServerApp();
698698
const emailItem = await app.getItem({ itemId: ITEM_IDS.emailsPerMonth, teamId: context.billingTeamId });
699699
const isDebited = await emailItem.tryDecreaseQuantity(1);

0 commit comments

Comments
 (0)