History

Switch billing from per-seat to flat-fee per org + add Slack/email contact

Pricing model - Replace seat-based pricing with org-flat-fee plans: Free (10k execs/mo, 3 members), Free PAYG (10k included + $0.20/1k overage), Team ($49/mo, 250k incl, unlimited members, $0.20/1k overage). Bumped Free included executions from 5k -> 10k for top-of-funnel. - Mark `seats` feature archived; introduce `members` (non-consumable metered) and `domain-verification` (boolean) features. - Update privacy/terms copy from "seat counts" to "membership" / "organization-level" pricing language. - Refresh /billing and /billing/plans UI: trim per-seat add-ons, render Free/Team/Enterprise cards, fix button alignment under conditional badges. Slack/email contact CTA on pricing page - New SlackService wraps Slack Web API: creates `#shared-<email-slug>` channel (retries with random suffix on `name_taken`), posts a hello message, and calls `conversations.inviteShared` with `external_limited`. - POST /api/contact/slack: public endpoint, validates email, verifies a Cloudflare Turnstile token, gates on a 5/min global ratelimit binding, then calls SlackService. Internal SlackError details are logged server-side; the client only sees a generic message. - Pricing page CTA: dialog with Turnstile widget + email/name/note form, prefilled from useAuth() when signed in. Sibling "Email us" mailto: link to rhys@executor.sh. - Wrangler bindings: ratelimit (5/60s), VITE_PUBLIC_TURNSTILE_SITEKEY vars; .env.op gets dev test keys + SLACK_BOT_TOKEN op:// reference.

Rhys Sullivan committed 6 months ago

Commit 47dfd6b1 parent c5d93a317 files · +919 −189Browse files

Showing the changes to apps/cloud/src/auth (1 of 17 files). Show all

Unified · Split

apps/cloud/src/auth/handlers.ts+9−8
@@ -10,6 +10,7 @@ import { authorizeOrganization } from "./authorize-organization"; · 9 unmodified lines
1010 import { env } from "cloudflare:workers";
1111 import { WorkOSError } from "./errors";
1212 import { WorkOSAuth } from "./workos";
13+import { AutumnService } from "../services/autumn";
1314
1415 const COOKIE_OPTIONS = {
1516 path: "/",
@@ -69,10 +70,8 @@ const setResponseCookie = ( · 53 unmodified lines
6970 options: typeof RESPONSE_COOKIE_OPTIONS,
7071 ) => HttpServerResponse.setCookieUnsafe(response, name, value, options);
7172
72−const deleteResponseCookie = (
73− response: HttpServerResponse.HttpServerResponse,
74− name: string,
75−) => HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS);
73+const deleteResponseCookie = (response: HttpServerResponse.HttpServerResponse, name: string) =>
74+ HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS);
7675
7776 // ---------------------------------------------------------------------------
7877 // Single non-protected API surface — public (login/callback) + session
@@ -208,7 +207,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 129 unmodified lines
208207 );
209208
210209 return {
211− organizations: organizations.filter((org): org is NonNullable<typeof org> => org !== null),
210+ organizations: organizations.filter(
211+ (org): org is NonNullable<typeof org> => org !== null,
212+ ),
212213 activeOrganizationId: session.organizationId,
213214 };
214215 }),
@@ -231,11 +232,13 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 16 unmodified lines
231232 Effect.gen(function* () {
232233 const workos = yield* WorkOSAuth;
233234 const users = yield* UserStoreService;
235+ const autumn = yield* AutumnService;
234236 const session = yield* SessionContext;
235237
236238 const name = payload.name.trim();
237239 const org = yield* workos.createOrganization(name);
238240 yield* workos.createMembership(org.id, session.accountId, "admin");
241+ yield* autumn.trackMemberChange(org.id, 1);
239242 yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name }));
240243
241244 // Try to attach the new org to the current session. This can fail
@@ -246,9 +249,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 4 unmodified lines
246249 // cookie and fail loudly; the frontend will bounce to login and
247250 // the callback's rehydrate path will pick up the new membership.
248251 const refreshed = yield* workos.refreshSession(session.sealedSession, org.id);
249− const verified = refreshed
250− ? yield* workos.authenticateSealedSession(refreshed)
251− : null;
252+ const verified = refreshed ? yield* workos.authenticateSealedSession(refreshed) : null;
252253
253254 if (!refreshed || !verified || verified.organizationId !== org.id) {
254255 yield* Effect.logWarning(

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t