Skip to content

Commit 7329d70

Browse files
Merge feat/free-tier-gate-and-badge into main
Free tier for public repos: - Workspace tier + repository_usage migration - WorkspaceTier, oss_free kind, RepositoryUsageRecord types - Repository usage tracking in DB layer - Installation webhook, visibility gate, rate limiting - "Reviewed by CodeVetter" badge footer on free-tier reviews - Thread reviewTier through review worker - Switch DB from Postgres to D1/Cloudflare - Remove dead tests, add free tier strategy doc
2 parents bd19bc1 + 54e257a commit 7329d70

17 files changed

Lines changed: 769 additions & 185 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"default":{"identifier":"default","description":"Default permissions for the main window","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:event:allow-emit","core:window:default","core:window:allow-close","core:window:allow-set-size","core:window:allow-set-title","dialog:default","dialog:allow-open","notification:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","process:allow-restart","updater:default","updater:allow-check","updater:allow-download-and-install"]}}
1+
{"default":{"identifier":"default","description":"Default permissions for the main window","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:event:allow-emit","core:window:default","core:window:allow-close","core:window:allow-set-size","core:window:allow-set-title","dialog:default","dialog:allow-open","notification:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:allow-notify","process:allow-restart","process:allow-exit"]}}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- Free tier support: workspace tiers and per-repo usage tracking
2+
3+
ALTER TABLE workspaces ADD COLUMN tier TEXT NOT NULL DEFAULT 'free';
4+
5+
-- Track monthly review counts per repository for rate limiting
6+
CREATE TABLE IF NOT EXISTS repository_usage (
7+
id TEXT PRIMARY KEY,
8+
repository_id TEXT NOT NULL REFERENCES repositories(id),
9+
period TEXT NOT NULL,
10+
review_count INTEGER NOT NULL DEFAULT 0,
11+
last_review_at TEXT,
12+
UNIQUE (repository_id, period)
13+
);
14+
15+
CREATE INDEX IF NOT EXISTS idx_repository_usage_repo ON repository_usage(repository_id);

packages/db/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@
99
"test": "node --test --import tsx src/**/*.test.ts"
1010
},
1111
"devDependencies": {
12-
"@types/pg": "^8.15.6",
12+
"@cloudflare/workers-types": "^4.20260329.1",
1313
"tsx": "^4.21.0"
1414
},
1515
"dependencies": {
16-
"@code-reviewer/shared-types": "file:../shared-types",
17-
"pg": "^8.16.3"
16+
"@code-reviewer/shared-types": "file:../shared-types"
1817
}
1918
}

packages/db/src/controlPlane.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
PullRequestRecord,
88
RepositoryConnection,
99
RepositoryRuleOverride,
10+
RepositoryUsageRecord,
1011
ReviewFindingRecord,
1112
ReviewRunRecord,
1213
SemanticChunkRecord,
@@ -253,6 +254,11 @@ export interface ControlPlaneDatabase {
253254
listSemanticChunks(repositoryId: string, sourceRef: string): Promise<SemanticChunkRecord[]>;
254255
deleteIndexedData(repositoryId: string, sourceRef?: string): Promise<{ filesDeleted: number; chunksDeleted: number }>;
255256
getIndexingStats(repositoryId: string): Promise<{ totalFiles: number; totalChunks: number; languages: Record<string, number>; lastIndexedAt?: string }>;
257+
258+
getRepositoryUsage(repositoryId: string, period: string): Promise<RepositoryUsageRecord | undefined>;
259+
incrementRepositoryUsage(repositoryId: string, period: string): Promise<RepositoryUsageRecord>;
260+
261+
getWorkspaceForRepository(repositoryId: string): Promise<WorkspaceRecord | undefined>;
256262
}
257263

258264
export class InMemoryControlPlaneDatabase implements ControlPlaneDatabase {
@@ -274,6 +280,7 @@ export class InMemoryControlPlaneDatabase implements ControlPlaneDatabase {
274280
private workspaceSecrets = new Map<string, WorkspaceSecretRecord>();
275281
private indexedFiles = new Map<string, IndexedFileRecord>();
276282
private semanticChunks = new Map<string, SemanticChunkRecord>();
283+
private repositoryUsage = new Map<string, RepositoryUsageRecord>();
277284

278285
async upsertUserFromGithub(input: UpsertGithubUserInput): Promise<UserRecord> {
279286
const existing = Array.from(this.users.values()).find(user => user.githubUserId === input.githubUserId);
@@ -375,6 +382,7 @@ export class InMemoryControlPlaneDatabase implements ControlPlaneDatabase {
375382
slug: input.slug,
376383
name: input.name,
377384
kind: input.kind,
385+
tier: input.kind === 'oss_free' ? 'free' : 'free',
378386
githubAccountType: input.githubAccountType,
379387
githubAccountId: input.githubAccountId,
380388
createdByUserId: input.createdByUserId,
@@ -966,4 +974,30 @@ export class InMemoryControlPlaneDatabase implements ControlPlaneDatabase {
966974

967975
return { totalFiles: files.length, totalChunks: chunks.length, languages, lastIndexedAt };
968976
}
977+
978+
async getRepositoryUsage(repositoryId: string, period: string): Promise<RepositoryUsageRecord | undefined> {
979+
const key = `${repositoryId}:${period}`;
980+
const record = this.repositoryUsage.get(key);
981+
return record ? clone(record) : undefined;
982+
}
983+
984+
async incrementRepositoryUsage(repositoryId: string, period: string): Promise<RepositoryUsageRecord> {
985+
const key = `${repositoryId}:${period}`;
986+
const existing = this.repositoryUsage.get(key);
987+
const record: RepositoryUsageRecord = {
988+
id: existing?.id || id('ru'),
989+
repositoryId,
990+
period,
991+
reviewCount: (existing?.reviewCount || 0) + 1,
992+
lastReviewAt: nowIso()
993+
};
994+
this.repositoryUsage.set(key, record);
995+
return clone(record);
996+
}
997+
998+
async getWorkspaceForRepository(repositoryId: string): Promise<WorkspaceRecord | undefined> {
999+
const repo = this.repositories.get(repositoryId);
1000+
if (!repo) return undefined;
1001+
return this.getWorkspaceById(repo.workspaceId);
1002+
}
9691003
}

packages/db/src/d1ControlPlane.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
PullRequestRecord,
88
RepositoryConnection,
99
RepositoryRuleOverride,
10+
RepositoryUsageRecord,
1011
ReviewFindingRecord,
1112
ReviewRunRecord,
1213
SemanticChunkRecord,
@@ -193,6 +194,7 @@ function mapWorkspace(row: Row): WorkspaceRecord {
193194
slug: String(row.slug),
194195
name: String(row.name),
195196
kind: row.kind as WorkspaceRecord['kind'],
197+
tier: (row.tier as WorkspaceRecord['tier']) || 'free',
196198
githubAccountType: (row.github_account_type as WorkspaceRecord['githubAccountType']) || undefined,
197199
githubAccountId: toOptionalString(row.github_account_id),
198200
createdByUserId: String(row.created_by_user_id),
@@ -1494,6 +1496,51 @@ export class D1ControlPlaneDatabase implements ControlPlaneDatabase {
14941496
lastIndexedAt: lastRow?.last_indexed as string | undefined
14951497
};
14961498
}
1499+
async getRepositoryUsage(repositoryId: string, period: string): Promise<RepositoryUsageRecord | undefined> {
1500+
const row = await this.queryOne<Row>(
1501+
`SELECT * FROM repository_usage WHERE repository_id = ?1 AND period = ?2 LIMIT 1`,
1502+
[repositoryId, period]
1503+
);
1504+
if (!row) return undefined;
1505+
return {
1506+
id: String(row.id),
1507+
repositoryId: String(row.repository_id),
1508+
period: String(row.period),
1509+
reviewCount: toNumber(row.review_count),
1510+
lastReviewAt: toOptionalString(row.last_review_at)
1511+
};
1512+
}
1513+
1514+
async incrementRepositoryUsage(repositoryId: string, period: string): Promise<RepositoryUsageRecord> {
1515+
const row = await this.queryOne<Row>(
1516+
`INSERT INTO repository_usage (id, repository_id, period, review_count, last_review_at)
1517+
VALUES (?1, ?2, ?3, 1, ?4)
1518+
ON CONFLICT (repository_id, period)
1519+
DO UPDATE SET review_count = review_count + 1, last_review_at = ?4
1520+
RETURNING *`,
1521+
[id('ru'), repositoryId, period, nowIso()]
1522+
);
1523+
if (!row) {
1524+
throw new Error('Failed to increment repository usage.');
1525+
}
1526+
return {
1527+
id: String(row.id),
1528+
repositoryId: String(row.repository_id),
1529+
period: String(row.period),
1530+
reviewCount: toNumber(row.review_count),
1531+
lastReviewAt: toOptionalString(row.last_review_at)
1532+
};
1533+
}
1534+
1535+
async getWorkspaceForRepository(repositoryId: string): Promise<WorkspaceRecord | undefined> {
1536+
const row = await this.queryOne<Row>(
1537+
`SELECT w.* FROM workspaces w
1538+
JOIN repositories r ON r.workspace_id = w.id
1539+
WHERE r.id = ?1 LIMIT 1`,
1540+
[repositoryId]
1541+
);
1542+
return row ? mapWorkspace(row) : undefined;
1543+
}
14971544
}
14981545

14991546
// ── Factory ────────────────────────────────────────────────────────────────────

packages/db/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './controlPlane';
2+
export * from './d1ControlPlane';
23
export * from './migrations';
3-
export * from './postgresControlPlane';
44
export * from './queryHelpers';
55
export * from './schema';

packages/db/src/migrations.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@ export const CONTROL_PLANE_MIGRATIONS: MigrationDefinition[] = [
77
{
88
id: '0001_init',
99
path: 'migrations/0001_init.sql'
10+
},
11+
{
12+
id: '0003_free_tier',
13+
path: 'migrations/0003_free_tier.sql'
1014
}
1115
];

packages/db/src/schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export const TABLES = {
1515
indexingRuns: 'indexing_runs',
1616
webhookEvents: 'webhook_events',
1717
auditLogs: 'audit_logs',
18-
workspaceSecrets: 'workspace_secrets'
18+
workspaceSecrets: 'workspace_secrets',
19+
repositoryUsage: 'repository_usage'
1920
} as const;
2021

2122
export type TableName = (typeof TABLES)[keyof typeof TABLES];

packages/db/tsconfig.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"strict": true,
99
"esModuleInterop": true,
1010
"skipLibCheck": true,
11-
"forceConsistentCasingInFileNames": true
11+
"forceConsistentCasingInFileNames": true,
12+
"types": ["@cloudflare/workers-types"]
1213
},
13-
"include": ["src/**/*"]
14+
"include": ["src/**/*"],
15+
"exclude": ["src/**/*.test.ts"]
1416
}

packages/review-core/src/formatting.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ReviewAction, ReviewFindingRecord } from '@code-reviewer/shared-types';
1+
import { ReviewAction, ReviewFindingRecord, WorkspaceTier } from '@code-reviewer/shared-types';
22
import { computeFindingFingerprint } from './scoring';
33

44
type StructuredReviewData = {
@@ -27,7 +27,8 @@ export function buildOverallBody(
2727
score: number,
2828
reviewRunId: string | undefined,
2929
action: ReviewAction,
30-
resolvedFindings?: ReviewFindingRecord[]
30+
resolvedFindings?: ReviewFindingRecord[],
31+
reviewTier?: WorkspaceTier
3132
): string {
3233
const counts: Record<string, number> = {
3334
critical: 0,
@@ -53,7 +54,17 @@ export function buildOverallBody(
5354
}
5455
}
5556

56-
body += `\n\n*Automated review by CodeVetter*`;
57+
// Footer: badge + CTA for free tier, clean for paid
58+
const tier = reviewTier || 'free';
59+
if (tier === 'free') {
60+
const scoreColor = score >= 80 ? 'brightgreen' : score >= 60 ? 'yellow' : 'red';
61+
body += `\n\n---\n`;
62+
body += `\n![Score](https://img.shields.io/badge/score-${score.toFixed(0)}%2F100-${scoreColor}) `;
63+
body += `**[Reviewed by CodeVetter](https://codevetter.com)**\n\n`;
64+
body += `*Free automated PR review for open source — [get CodeVetter for your repo](https://codevetter.com/install)*`;
65+
} else {
66+
body += `\n\n*Automated review by CodeVetter*`;
67+
}
5768

5869
// Embed structured data for agents
5970
if (reviewRunId) {

0 commit comments

Comments
 (0)