ワンクリックで
edition
Work with the Sayr edition system (cloud, community, enterprise) including limits, capabilities, and edition-aware code
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Work with the Sayr edition system (cloud, community, enterprise) including limits, capabilities, and edition-aware code
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
| name | edition |
| description | Work with the Sayr edition system (cloud, community, enterprise) including limits, capabilities, and edition-aware code |
| metadata | {"audience":"developers","workflow":"feature-development"} |
I help you work with the @repo/edition package -- the single source of truth for what edition of Sayr is running (cloud, community, enterprise) and what capabilities/limits are available. This includes adding new capabilities, enforcing resource limits, making UI edition-aware, and configuring Docker builds.
Sayr ships in three editions:
| Edition | Description | How it runs |
|---|---|---|
cloud | Hosted SaaS at sayr.io | sayr-{start,backend,worker,marketing} Docker images (private) |
community | Free self-hosted (CE) | sayr-ce-{start,backend,worker} Docker images (public) |
enterprise | Licensed self-hosted | Same CE images + SAYR_LICENSE_KEY env var |
Key design principles:
SAYR_EDITION_BAKED -- CE images cannot be switched to cloud mode at runtimeSAYR_EDITION env var controls the edition (no baked value)@repo/edition is used by both frontend and backend1. SAYR_EDITION_BAKED (build-time, Docker only)
├── "cloud" → cloud
└── "community" + valid license → enterprise
└── "community" (no license) → community
2. SAYR_EDITION env var (local dev)
3. SAYR_CLOUD=true (legacy compat → cloud)
4. Default → community
| File | Purpose |
|---|---|
packages/edition/src/types.ts | Edition, PlanLimits, EditionCapabilities, PlanId types |
packages/edition/src/edition.ts | getEdition(), isCloud(), isSelfHosted(), isCommunity(), isEnterprise() |
packages/edition/src/capabilities.ts | getEditionCapabilities(), getEffectiveLimits(), canCreateResource(), getLimitReachedMessage() |
packages/edition/src/index.ts | Barrel exports |
apps/start/vite.config.ts | Bakes VITE_SAYR_EDITION into frontend via define block |
apps/backend/Dockerfile | ARG SAYR_EDITION_BAKED before build step |
apps/start/Dockerfile | ARG SAYR_EDITION_BAKED before build step |
apps/worker/Dockerfile | ARG SAYR_EDITION_BAKED before build step |
.github/workflows/build-and-deploy.yml | edition: [cloud, ce] matrix dimension |
| File | What it checks |
|---|---|
apps/backend/routes/api/internal/v1/organization.ts | Org creation limit, saved view limit, issue template limit, team limit |
apps/backend/routes/api/internal/v1/release.ts | Release creation limit |
packages/opentelemetry/src/index.ts | axiomTelemetryEnabled for telemetry output |
apps/start/src/routes/orgs/$orgSlug/route.tsx | multiTenantEnabled for public org resolution |
apps/start/src/lib/routemap.ts | Filters cloudOnly nav items from settings sidebar |
apps/start/src/routes/(admin)/settings/org/$orgId/billing/index.tsx | Redirects non-cloud to general settings |
apps/start/src/components/admin/sidebars/user-dropdown.tsx | Edition label display, hide billing on CE |
apps/start/src/components/organization/CreateOrganizationDialog.tsx | Returns null on CE when user has >= 1 org |
packages/auth/src/index.ts | First-user auto-admin on self-hosted editions |
edition.ts)import { getEdition, isCloud, isSelfHosted, isCommunity, isEnterprise } from "@repo/edition";
getEdition(); // "cloud" | "community" | "enterprise"
isCloud(); // true only on cloud
isSelfHosted(); // true on community OR enterprise
isCommunity(); // true only on community
isEnterprise(); // true only on enterprise
capabilities.ts)import { getEditionCapabilities, getEffectiveLimits, canCreateResource, getLimitReachedMessage } from "@repo/edition";
// Instance-wide capabilities (not per-org)
const caps = getEditionCapabilities();
caps.maxOrganizations; // number | null (null = unlimited)
caps.polarBillingEnabled; // boolean
caps.dorasOAuthEnabled; // boolean
caps.axiomTelemetryEnabled; // boolean
caps.marketingSiteEnabled; // boolean
caps.multiTenantEnabled; // boolean
// Per-org limits based on edition + plan
const limits = getEffectiveLimits("free"); // pass org.plan
limits.members; // number | null
limits.savedViews; // number | null
limits.issueTemplates; // number | null
limits.teams; // number | null
limits.releases; // number | null
// Check if a resource can be created
canCreateResource("savedViews", currentCount, org.plan); // boolean
// Human-readable error message
getLimitReachedMessage("teams", org.plan); // "You've reached the maximum..."
types.ts)import type { Edition, PlanLimits, EditionCapabilities, CloudPlan, SelfHostedPlan, PlanId } from "@repo/edition";
type Edition = "cloud" | "community" | "enterprise";
type CloudPlan = "free" | "pro";
type SelfHostedPlan = "self-hosted";
type PlanId = CloudPlan | SelfHostedPlan;
Every resource creation endpoint follows this pattern. Fetch the org with plan data, count existing resources, check limits.
// In a Hono route handler
import { canCreateResource, getLimitReachedMessage } from "@repo/edition";
import { count, eq, and } from "drizzle-orm";
// 1. Fetch org with plan
const org = await db.query.organization.findFirst({
where: eq(schema.organization.id, orgId),
columns: { id: true, plan: true },
});
// 2. Count existing resources (exclude system resources if applicable)
const [{ count: existingCount }] = await db
.select({ count: count() })
.from(schema.myResource)
.where(eq(schema.myResource.organizationId, orgId));
// 3. Check limit
if (!canCreateResource("myResource", existingCount, org.plan)) {
return c.json({
success: false,
error: getLimitReachedMessage("myResource", org.plan),
}, 403);
}
For instance-level capabilities (not per-org), use getEditionCapabilities().
import { getEditionCapabilities } from "@repo/edition";
const caps = getEditionCapabilities();
// Example: org creation limit
if (caps.maxOrganizations !== null && userOrgCount >= caps.maxOrganizations) {
return c.json({ success: false, error: "Organization limit reached" }, 403);
}
For backend/server functions, import directly from @repo/edition.
import { isCloud, getEditionCapabilities } from "@repo/edition";
// In a TanStack Start loader or server function
if (!isCloud()) {
throw redirect({ to: "/settings/org/$orgId", params: { orgId } });
}
On the client, read the build-time baked value from import.meta.env.VITE_SAYR_EDITION.
// Client-side component
const editionRaw = import.meta.env.VITE_SAYR_EDITION as string | undefined;
// Hide billing on non-cloud
{editionRaw === "cloud" && <BillingMenuItem />}
// Show edition label
const editionLabel = editionRaw === "cloud" ? "Cloud"
: editionRaw === "enterprise" ? "Enterprise"
: "Community";
Do NOT import @repo/edition in client components -- it uses process.env which is server-only. Use the Vite env var instead.
Add cloudOnly: true to nav entries in routemap.ts and filter them.
// In routemap.ts
{ name: "Billing", to: "/billing", icon: IconCreditCard, cloudOnly: true },
// Filter
export const orgSettingsNavigation = allNavItems.filter((item) => {
if (item.cloudOnly && !isCloud()) return false;
return true;
});
Each Dockerfile uses ARG/ENV SAYR_EDITION_BAKED in the installer stage before pnpm build:
# In the installer stage (before build)
ARG SAYR_EDITION_BAKED="community"
ENV SAYR_EDITION_BAKED=${SAYR_EDITION_BAKED}
RUN pnpm build # Edition is baked into the JS bundle here
The CI workflow passes the appropriate value:
SAYR_EDITION_BAKED=cloudSAYR_EDITION_BAKED=communityEditionCapabilities in types.tsEDITION_CAPABILITIES in capabilities.tsgetEditionCapabilities().myNewCapabilityPlanLimits in types.tsCLOUD_PLAN_LIMITS, SELF_HOSTED_LIMITS, and FREE_LIMITS in capabilities.tscanCreateResource()| Capability | Cloud | Community | Enterprise |
|---|---|---|---|
maxOrganizations | unlimited | 1 | unlimited |
polarBillingEnabled | true | false | false |
dorasOAuthEnabled | true | false | false |
axiomTelemetryEnabled | true | false | false |
marketingSiteEnabled | true | false | false |
multiTenantEnabled | true | false | false |
| Resource | Cloud Free | Cloud Pro | Self-Hosted (CE/Enterprise) |
|---|---|---|---|
members | 5 | 1000 | 1000 |
savedViews | 3 | unlimited | unlimited |
issueTemplates | 3 | unlimited | unlimited |
teams | 1 | unlimited | unlimited |
releases | 0 | unlimited | unlimited |
pnpm dev:ce # Run locally as community edition
pnpm dev:ce:op # Same, with 1Password secret injection
pnpm dev:cloud # Run locally as cloud edition
pnpm dev:cloud:op # Same, with 1Password secret injection
These use cross-env to set SAYR_EDITION and VITE_SAYR_EDITION, passed through by turbo.json's globalPassThroughEnv.
@repo/edition in client-side React components -- it uses process.env. Use import.meta.env.VITE_SAYR_EDITION instead.@repo/edition directly.SAYR_EDITION_BAKED build arg must appear BEFORE pnpm build in Dockerfiles so it's compiled into the bundle.null means unlimited in both PlanLimits and EditionCapabilities.isSystem: true) should not count toward the team limit.@repo/edition dependency to package.json of any app/package that imports from it.Use this skill when:
Tell me:
SOC 職業分類に基づく
Create or update user-facing documentation for Sayr features in apps/marketing
Add, remove, or modify commands and sub-views in the Cmd+K command palette
Add or modify PageHeader layouts on admin pages, including task view integration and cross-org patterns
Add, modify, or troubleshoot the right sidebar panel on admin pages
Update GitHub pull request title and description with comprehensive summary