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
10import { env } from "cloudflare:workers";10import { env } from "cloudflare:workers";
11import { WorkOSError } from "./errors";11import { WorkOSError } from "./errors";
12import { WorkOSAuth } from "./workos";12import { WorkOSAuth } from "./workos";
13import { AutumnService } from "../services/autumn";
13 14
14const COOKIE_OPTIONS = {15const COOKIE_OPTIONS = {
15 path: "/",16 path: "/",
@@ -69,10 +70,8 @@ const setResponseCookie = ( · 53 unmodified lines
69 options: typeof RESPONSE_COOKIE_OPTIONS,70 options: typeof RESPONSE_COOKIE_OPTIONS,
70) => HttpServerResponse.setCookieUnsafe(response, name, value, options);71) => HttpServerResponse.setCookieUnsafe(response, name, value, options);
71 72
72const deleteResponseCookie = (73const deleteResponseCookie = (response: HttpServerResponse.HttpServerResponse, name: string) =>
73 response: HttpServerResponse.HttpServerResponse,74 HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS);
74 name: string,
75) => HttpServerResponse.setCookieUnsafe(response, name, "", DELETE_COOKIE_OPTIONS);
76 75
77// ---------------------------------------------------------------------------76// ---------------------------------------------------------------------------
78// Single non-protected API surface — public (login/callback) + session77// Single non-protected API surface — public (login/callback) + session
@@ -208,7 +207,9 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 129 unmodified lines
208 );207 );
209 208
210 return {209 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 ),
212 activeOrganizationId: session.organizationId,213 activeOrganizationId: session.organizationId,
213 };214 };
214 }),215 }),
@@ -231,11 +232,13 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group( · 16 unmodified lines
231 Effect.gen(function* () {232 Effect.gen(function* () {
232 const workos = yield* WorkOSAuth;233 const workos = yield* WorkOSAuth;
233 const users = yield* UserStoreService;234 const users = yield* UserStoreService;
235 const autumn = yield* AutumnService;
234 const session = yield* SessionContext;236 const session = yield* SessionContext;
235 237
236 const name = payload.name.trim();238 const name = payload.name.trim();
237 const org = yield* workos.createOrganization(name);239 const org = yield* workos.createOrganization(name);
238 yield* workos.createMembership(org.id, session.accountId, "admin");240 yield* workos.createMembership(org.id, session.accountId, "admin");
241 yield* autumn.trackMemberChange(org.id, 1);
239 yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name }));242 yield* users.use((s) => s.upsertOrganization({ id: org.id, name: org.name }));
240 243
241 // Try to attach the new org to the current session. This can fail244 // 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
246 // cookie and fail loudly; the frontend will bounce to login and249 // cookie and fail loudly; the frontend will bounce to login and
247 // the callback's rehydrate path will pick up the new membership.250 // the callback's rehydrate path will pick up the new membership.
248 const refreshed = yield* workos.refreshSession(session.sealedSession, org.id);251 const refreshed = yield* workos.refreshSession(session.sealedSession, org.id);
249 const verified = refreshed252 const verified = refreshed ? yield* workos.authenticateSealedSession(refreshed) : null;
250 ? yield* workos.authenticateSealedSession(refreshed)
251 : null;
252 253
253 if (!refreshed || !verified || verified.organizationId !== org.id) {254 if (!refreshed || !verified || verified.organizationId !== org.id) {
254 yield* Effect.logWarning(255 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