import type { Organization, OrgMember, Prisma, Project, RuntimeEnvironment, User, } from "@trigger.dev/database"; import { customAlphabet } from "nanoid"; import { generate } from "random-words"; import slug from "slug"; import { $replica, prisma, type PrismaClientOrTransaction } from "~/db.server"; import { env } from "~/env.server"; import { featuresForUrl } from "~/features.server"; import { createApiKeyForEnv, createPkApiKeyForEnv, envSlug } from "./api-key.server"; import { getDefaultEnvironmentConcurrencyLimit } from "~/services/platform.v3.server"; import { enqueueAttioWorkspaceSync } from "~/services/attio.server"; import { applyBillingLimitPauseAfterEnvCreate, getInitialEnvPauseStateForBillingLimit, } from "~/v3/services/billingLimit/getInitialEnvPauseStateForBillingLimit.server"; export type { Organization }; const nanoid = customAlphabet("1234567890abcdef", 4); /** * Resolve an organization id from its slug for use as an RBAC auth scope. * Reads the replica first (the common case) and falls back to the primary on a * miss, so replica lag never leaves a real org unresolved, which the dashboard * route builder treats as an unauthorized request. */ export async function resolveOrgIdFromSlug(slug: string): Promise { const fromReplica = await $replica.organization.findFirst({ where: { slug }, select: { id: true }, }); if (fromReplica) { return fromReplica.id; } const fromPrimary = await prisma.organization.findFirst({ where: { slug }, select: { id: true }, }); return fromPrimary?.id ?? null; } export async function createOrganization( { title, userId, companySize, onboardingData, avatar, }: Pick & { userId: User["id"]; onboardingData?: Prisma.InputJsonValue; avatar?: Prisma.InputJsonValue; }, attemptCount = 0 ): Promise { if (typeof process.env.BLOCKED_USERS === "string" && process.env.BLOCKED_USERS.includes(userId)) { throw new Error("Organization could not be created."); } const uniqueOrgSlug = `${slug(title)}-${nanoid(4)}`; const orgWithSameSlug = await prisma.organization.findFirst({ where: { slug: uniqueOrgSlug }, }); if (attemptCount > 100) { throw new Error(`Unable to create organization with slug ${uniqueOrgSlug} after 100 attempts`); } if (orgWithSameSlug) { return createOrganization( { title, userId, companySize, onboardingData, avatar, }, attemptCount + 1 ); } const features = featuresForUrl(new URL(env.APP_ORIGIN)); const organization = await prisma.organization.create({ data: { title, slug: uniqueOrgSlug, companySize, onboardingData: onboardingData ?? undefined, avatar: avatar ?? undefined, maximumConcurrencyLimit: env.DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT, members: { create: { userId: userId, role: "ADMIN", }, }, // Managed-cloud orgs start deactivated so they're routed through // select-plan, which provisions their billing entitlement and activates // them. Self-hosters have no billing gate, so they're active immediately. isActivated: !features.isManagedCloud, }, include: { members: true, }, }); // Fire-and-forget; never blocks org creation. void enqueueAttioWorkspaceSync({ orgId: organization.id, title: organization.title, slug: organization.slug, companySize: organization.companySize, createdAt: organization.createdAt, adminUserId: userId, }); return { ...organization }; } export async function createEnvironment({ organization, project, type, isBranchableEnvironment = false, member, prismaClient = prisma, /** When set, skips billing lookup — caller must supply the limit for this org + type. */ maximumConcurrencyLimit, }: { organization: Pick; project: Pick; type: RuntimeEnvironment["type"]; isBranchableEnvironment?: boolean; member?: OrgMember; prismaClient?: PrismaClientOrTransaction; maximumConcurrencyLimit?: number; }) { const slug = envSlug(type); const apiKey = createApiKeyForEnv(type); const pkApiKey = createPkApiKeyForEnv(type); const shortcode = createShortcode().join("-"); const limit = maximumConcurrencyLimit ?? (await getDefaultEnvironmentConcurrencyLimit(organization.id, type)); const billingPause = await getInitialEnvPauseStateForBillingLimit(organization.id, type); const environment = await prismaClient.runtimeEnvironment.create({ data: { slug, apiKey, pkApiKey, shortcode, autoEnableInternalSources: type !== "DEVELOPMENT", maximumConcurrencyLimit: limit, paused: billingPause.paused, pauseSource: billingPause.pauseSource, organization: { connect: { id: organization.id, }, }, project: { connect: { id: project.id, }, }, orgMember: member ? { connect: { id: member.id } } : undefined, type, isBranchableEnvironment, }, include: { organization: true, project: true, }, }); await applyBillingLimitPauseAfterEnvCreate(environment); return environment; } function createShortcode() { return generate({ exactly: 2 }); }