Free/Pro plan limitations

This commit is contained in:
2026-01-28 22:12:32 +00:00
parent c0e06ac8ba
commit 7f3cb7c890
15 changed files with 420 additions and 60 deletions

View File

@@ -7,3 +7,11 @@ export * from "./sprints";
export * from "./subscriptions";
export * from "./timed-sessions";
export * from "./users";
// free tier limits
export const FREE_TIER_LIMITS = {
organisationsPerUser: 1,
projectsPerOrganisation: 1,
issuesPerOrganisation: 100,
membersPerOrganisation: 5,
} as const;

View File

@@ -259,6 +259,25 @@ export async function getIssueAssigneeCount(issueId: number): Promise<number> {
return result?.count ?? 0;
}
export async function getOrganisationIssueCount(organisationId: number): Promise<number> {
const { Project } = await import("@sprint/shared");
const projects = await db
.select({ id: Project.id })
.from(Project)
.where(eq(Project.organisationId, organisationId));
const projectIds = projects.map((p) => p.id);
if (projectIds.length === 0) return 0;
const [result] = await db
.select({ count: sql<number>`COUNT(*)` })
.from(Issue)
.where(inArray(Issue.projectId, projectIds));
return result?.count ?? 0;
}
export async function isIssueAssignee(issueId: number, userId: number): Promise<boolean> {
const [assignee] = await db
.select({ id: IssueAssignee.id })

View File

@@ -1,5 +1,5 @@
import { Organisation, OrganisationMember, User } from "@sprint/shared";
import { and, eq } from "drizzle-orm";
import { and, eq, sql } from "drizzle-orm";
import { db } from "../client";
export async function createOrganisation(name: string, slug: string, description?: string) {
@@ -144,3 +144,11 @@ export async function updateOrganisationMemberRole(organisationId: number, userI
.returning();
return member;
}
export async function getUserOrganisationCount(userId: number): Promise<number> {
const [result] = await db
.select({ count: sql<number>`COUNT(*)` })
.from(OrganisationMember)
.where(eq(OrganisationMember.userId, userId));
return result?.count ?? 0;
}

View File

@@ -1,5 +1,5 @@
import { Issue, Organisation, Project, Sprint, User } from "@sprint/shared";
import { eq } from "drizzle-orm";
import { eq, sql } from "drizzle-orm";
import { db } from "../client";
export async function createProject(key: string, name: string, creatorId: number, organisationId: number) {
@@ -82,3 +82,11 @@ export async function getProjectsByOrganisationId(organisationId: number) {
.leftJoin(Organisation, eq(Project.organisationId, Organisation.id));
return projects;
}
export async function getOrganisationProjectCount(organisationId: number): Promise<number> {
const [result] = await db
.select({ count: sql<number>`COUNT(*)` })
.from(Project)
.where(eq(Project.organisationId, organisationId));
return result?.count ?? 0;
}

View File

@@ -1,6 +1,13 @@
import { IssueCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import { createIssue, getOrganisationMemberRole, getProjectByID } from "../../db/queries";
import {
createIssue,
FREE_TIER_LIMITS,
getOrganisationIssueCount,
getOrganisationMemberRole,
getProjectByID,
getUserById,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function issueCreate(req: AuthedRequest) {
@@ -26,6 +33,19 @@ export default async function issueCreate(req: AuthedRequest) {
);
}
// check free tier limit
const user = await getUserById(req.userId);
if (user && user.plan !== "pro") {
const issueCount = await getOrganisationIssueCount(project.organisationId);
if (issueCount >= FREE_TIER_LIMITS.issuesPerOrganisation) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.issuesPerOrganisation} issues per organisation. upgrade to pro for unlimited issues.`,
"FREE_TIER_ISSUE_LIMIT",
403,
);
}
}
const issue = await createIssue(
project.id,
title,

View File

@@ -2,8 +2,10 @@ import { OrgAddMemberRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import {
createOrganisationMember,
FREE_TIER_LIMITS,
getOrganisationById,
getOrganisationMemberRole,
getOrganisationMembers,
getUserById,
} from "../../db/queries";
import { updateSeatCount } from "../../lib/seats";
@@ -39,6 +41,19 @@ export default async function organisationAddMember(req: AuthedRequest) {
return errorResponse("only owners and admins can add members", "PERMISSION_DENIED", 403);
}
// check free tier member limit
const requester = await getUserById(req.userId);
if (requester && requester.plan !== "pro") {
const members = await getOrganisationMembers(organisationId);
if (members.length >= FREE_TIER_LIMITS.membersPerOrganisation) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.membersPerOrganisation} members per organisation. upgrade to pro for unlimited members.`,
"FREE_TIER_MEMBER_LIMIT",
403,
);
}
}
const member = await createOrganisationMember(organisationId, userId, role);
// update seat count if the requester is the owner

View File

@@ -1,6 +1,12 @@
import { OrgCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import { createOrganisationWithOwner, getOrganisationBySlug } from "../../db/queries";
import {
createOrganisationWithOwner,
FREE_TIER_LIMITS,
getOrganisationBySlug,
getUserById,
getUserOrganisationCount,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function organisationCreate(req: AuthedRequest) {
@@ -14,6 +20,19 @@ export default async function organisationCreate(req: AuthedRequest) {
return errorResponse(`organisation with slug "${slug}" already exists`, "SLUG_TAKEN", 409);
}
// check free tier limit
const user = await getUserById(req.userId);
if (user && user.plan !== "pro") {
const orgCount = await getUserOrganisationCount(req.userId);
if (orgCount >= FREE_TIER_LIMITS.organisationsPerUser) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.organisationsPerUser} organisation. upgrade to pro for unlimited organisations.`,
"FREE_TIER_ORG_LIMIT",
403,
);
}
}
const organisation = await createOrganisationWithOwner(name, slug, req.userId, description);
return Response.json(organisation);

View File

@@ -1,6 +1,13 @@
import { ProjectCreateRequestSchema } from "@sprint/shared";
import type { AuthedRequest } from "../../auth/middleware";
import { createProject, getOrganisationMemberRole, getProjectByKey, getUserById } from "../../db/queries";
import {
createProject,
FREE_TIER_LIMITS,
getOrganisationMemberRole,
getOrganisationProjectCount,
getProjectByKey,
getUserById,
} from "../../db/queries";
import { errorResponse, parseJsonBody } from "../../validation";
export default async function projectCreate(req: AuthedRequest) {
@@ -22,7 +29,19 @@ export default async function projectCreate(req: AuthedRequest) {
return errorResponse("only owners and admins can create projects", "PERMISSION_DENIED", 403);
}
// check free tier limit
const creator = await getUserById(req.userId);
if (creator && creator.plan !== "pro") {
const projectCount = await getOrganisationProjectCount(organisationId);
if (projectCount >= FREE_TIER_LIMITS.projectsPerOrganisation) {
return errorResponse(
`free tier is limited to ${FREE_TIER_LIMITS.projectsPerOrganisation} project per organisation. upgrade to pro for unlimited projects.`,
"FREE_TIER_PROJECT_LIMIT",
403,
);
}
}
if (!creator) {
return errorResponse(`creator not found`, "CREATOR_NOT_FOUND", 404);
}