Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/post-upgrade-maintenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Post-Upgrade Maintenance
permissions:
contents: read

on:
workflow_dispatch:
inputs:
environment:
description: 'Environment to run maintenance on'
required: true
type: choice
options:
- staging
- prod

jobs:
maintenance-staging:
name: Run Staging Post-Upgrade Maintenance
runs-on: ubuntu-latest
timeout-minutes: 120
environment: staging
if: github.event.inputs.environment == 'staging'

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run post-upgrade maintenance (Staging)
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_SSL: ${{ secrets.DATABASE_SSL }}
run: bun run post-upgrade:maintenance

maintenance-prod:
name: Run Prod Post-Upgrade Maintenance
runs-on: ubuntu-latest
timeout-minutes: 120
environment: prod
if: github.event.inputs.environment == 'prod'

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Run post-upgrade maintenance (Prod)
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
DATABASE_SSL: ${{ secrets.DATABASE_SSL }}
run: bun run post-upgrade:maintenance
6 changes: 3 additions & 3 deletions app/(api)/api/jobs/blobs/delete-owner-blobs/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { serve } from "@upstash/workflow/nextjs";
import { inArray } from "drizzle-orm";
import { blob } from "@/drizzle/schema";
import { deleteFile, listFiles } from "@/lib/blobStore";
import { getDatabaseForOwner } from "@/lib/utils/useDatabase";
import { triggerWorkflow } from "@/lib/utils/workflow";
import { serve } from "@upstash/workflow/nextjs";
import { inArray } from "drizzle-orm";

type WorkflowPayload = {
ownerId: string;
Expand Down Expand Up @@ -117,7 +117,7 @@ export const { POST } = serve<WorkflowPayload>(async (context) => {
.execute();

console.log(
`[BlobDeletion] Cleaned up ${result.rowCount || 0} blob records from database for owner: ${ownerId}`,
`[BlobDeletion] Cleaned up ${result.count || 0} blob records from database for owner: ${ownerId}`,
);
} catch (error) {
console.error(
Expand Down
25 changes: 15 additions & 10 deletions app/(api)/api/webhook/auth/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import path from "node:path";
import { verifyWebhook } from "@clerk/nextjs/webhooks";
import { eq, sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import type { NextRequest } from "next/server";
import { Resend } from "resend";
import {
Expand Down Expand Up @@ -48,25 +48,30 @@ async function createTenantDatabase(ownerId: string): Promise<void> {
},
);

const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : "";

const ownerDb = drizzle(`${process.env.DATABASE_URL}/manage${sslMode}`, {
const ownerDb = drizzle({
connection: {
url: `${process.env.DATABASE_URL}/manage`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
});

const checkDb = await ownerDb.execute(
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);

if (checkDb.rowCount === 0) {
if (checkDb.count === 0) {
await ownerDb.execute(sql`CREATE DATABASE ${sql.identifier(databaseName)}`);
console.log(`Created database for tenant: ${databaseName}`);
}

const tenantDb = drizzle(
`${process.env.DATABASE_URL}/${databaseName}${sslMode}`,
{ schema },
);
const tenantDb = drizzle({
connection: {
url: `${process.env.DATABASE_URL}/${databaseName}`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
});

const migrationsFolder = path.resolve(process.cwd(), "drizzle");
await migrate(tenantDb, { migrationsFolder });
Expand Down
336 changes: 208 additions & 128 deletions bun.lock

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion components/layout/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Link from "next/link";

const navigation = {
main: [
{ name: "Status", href: "https://manage.openstatus.dev" },
{ name: "Terms", href: "/terms" },
{ name: "Privacy", href: "/privacy" },
{ name: "Source code", href: "https://github.com/techulus/manage" },
Expand Down
4 changes: 2 additions & 2 deletions drizzle/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { InferSelectModel } from "drizzle-orm";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type * as dbSchema from "./schema";
import type {
activity,
Expand All @@ -12,7 +12,7 @@ import type {
user,
} from "./schema";

export type Database = NodePgDatabase<typeof dbSchema>;
export type Database = PostgresJsDatabase<typeof dbSchema>;

export type User = InferSelectModel<typeof user>;
export type Project = InferSelectModel<typeof project>;
Expand Down
51 changes: 14 additions & 37 deletions lib/utils/useDatabase.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { attachDatabasePool } from "@vercel/functions";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import { err, ok, type Result } from "neverthrow";
import { Pool } from "pg";
import type { Database } from "@/drizzle/types";
import * as schema from "../../drizzle/schema";
import { getOwner } from "./useOwner";

const poolInstances = new Map<string, Pool>();

export function getDatabaseName(ownerId: string): Result<string, string> {
if (!ownerId.startsWith("org_") && !ownerId.startsWith("user_")) {
return err("Invalid owner ID");
Expand All @@ -33,22 +29,13 @@ export async function getDatabaseForOwner(ownerId: string): Promise<Database> {
},
);

if (!poolInstances.has(ownerId)) {
const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : "";
const connectionString = `${process.env.DATABASE_URL}/${databaseName}${sslMode}`;

const pool = new Pool({
connectionString,
min: 1,
idleTimeoutMillis: 5000,
connectionTimeoutMillis: 5000,
});

attachDatabasePool(pool);
poolInstances.set(ownerId, pool);
}

return drizzle(poolInstances.get(ownerId)!, { schema });
return drizzle({
connection: {
url: `${process.env.DATABASE_URL}/${databaseName}`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
});
}

export async function deleteDatabase(ownerId: string) {
Expand All @@ -59,22 +46,14 @@ export async function deleteDatabase(ownerId: string) {
},
);

const pool = poolInstances.get(ownerId);
if (pool) {
await pool.end();
poolInstances.delete(ownerId);
}

const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : "";
const managePool = new Pool({
connectionString: `${process.env.DATABASE_URL}/manage${sslMode}`,
min: 1,
idleTimeoutMillis: 5000,
connectionTimeoutMillis: 5000,
const ownerDb = drizzle({
connection: {
url: `${process.env.DATABASE_URL}/${databaseName}`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
});

const ownerDb = drizzle(managePool, { schema });

await ownerDb.execute(sql`
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
Expand All @@ -85,6 +64,4 @@ export async function deleteDatabase(ownerId: string) {
await ownerDb.execute(
sql`DROP DATABASE ${sql.identifier(databaseName)} WITH (FORCE)`,
);

await managePool.end();
}
1 change: 1 addition & 0 deletions next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { withSentryConfig } from "@sentry/nextjs";

const nextConfig = {
typedRoutes: false,
reactCompiler: true,

rewrites: async () => {
return [
Expand Down
4 changes: 2 additions & 2 deletions ops/drizzle/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type * as dbSchema from "./schema";

export type OpsDatabase = NodePgDatabase<typeof dbSchema>;
export type OpsDatabase = PostgresJsDatabase<typeof dbSchema>;
30 changes: 8 additions & 22 deletions ops/useOps.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
import { attachDatabasePool } from "@vercel/functions";
import type { UserJSON } from "@clerk/nextjs/server";
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { drizzle } from "drizzle-orm/postgres-js";
import * as schema from "./drizzle/schema";
import type { OpsDatabase } from "./drizzle/types";

let opsPool: Pool | null = null;
let opsDb: OpsDatabase | null = null;

export async function getOpsDatabase(): Promise<OpsDatabase> {
if (!opsDb) {
const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : "";
const connectionString = `${process.env.DATABASE_URL}/manage${sslMode}`;

opsPool = new Pool({
connectionString,
min: 1,
idleTimeoutMillis: 5000,
connectionTimeoutMillis: 5000,
});

attachDatabasePool(opsPool);
opsDb = drizzle(opsPool, { schema });
}

return opsDb;
return drizzle({
connection: {
url: `${process.env.DATABASE_URL}/manage`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
});
}

export async function addUserToOpsDb(userData: UserJSON) {
Expand Down
17 changes: 9 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
"fix": "biome format --write && biome lint --write",
"generate:migrations": "drizzle-kit generate",
"migrate:tenants": "bun scripts/migrate-all-tenants.ts",
"migrate:ops": "bun scripts/migrate-ops.ts"
"migrate:ops": "bun scripts/migrate-ops.ts",
"post-upgrade:maintenance": "bun scripts/post-upgrade-maintenance.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.623.0",
Expand Down Expand Up @@ -41,8 +42,8 @@
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "^1.2.3",
"@react-email/components": "^0.1.1",
"@sentry/nextjs": "^10",
"@sentry/node": "^10.10.0",
"@sentry/nextjs": "^10.21.0",
"@sentry/node": "^10.21.0",
"@tanstack/react-query": "^5.71.10",
"@trpc/client": "^11.0.2",
"@trpc/server": "^11.0.2",
Expand All @@ -52,8 +53,8 @@
"@upstash/redis": "^1.35.3",
"@upstash/search": "^0.1.5",
"@upstash/workflow": "^0.2.16",
"@vercel/functions": "^3.1.4",
"autoprefixer": "10.4.14",
"babel-plugin-react-compiler": "^1.0.0",
"caniuse-lite": "^1.0.30001737",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -64,17 +65,17 @@
"dotenv": "^16.4.7",
"drizzle-orm": "^0.44.6",
"es-toolkit": "^1.39.8",
"eslint-config-next": "15.5.5",
"eslint-config-next": "16.0.0",
"ical-generator": "^8.0.1",
"lucide-react": "^0.503.0",
"mime-types": "^2.1.35",
"neverthrow": "^8.2.0",
"next": "15.5.5",
"next": "16.0.0",
"next-themes": "^0.3.0",
"node-ical": "^0.20.1",
"nuqs": "^2.4.1",
"pg": "^8.14.1",
"postcss": "8.4.23",
"postgres": "^3.4.7",
"posthog-js": "^1.242.2",
"posthog-node": "^4.17.1",
"react": "19.2.0",
Expand Down Expand Up @@ -105,7 +106,7 @@
"@types/node": "20.1.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"drizzle-kit": "^0.31.4",
"drizzle-kit": "^0.31.5",
"encoding": "^0.1.13",
"typescript": "5.7.2"
},
Expand Down
File renamed without changes.
15 changes: 9 additions & 6 deletions scripts/migrate-all-tenants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import path from "node:path";
import { sql } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
import { migrate } from "drizzle-orm/node-postgres/migrator";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import * as schema from "../drizzle/schema";
import * as opsSchema from "../ops/drizzle/schema";

Expand All @@ -22,9 +22,12 @@ function sanitizeError(error: string, tenantId: string): string {
}

async function getOpsDatabase() {
const sslMode = process.env.DATABASE_SSL === "true" ? "?sslmode=require" : "";
return drizzle(`${process.env.DATABASE_URL}/manage${sslMode}`, {
schema: opsSchema,
return drizzle({
connection: {
url: `${process.env.DATABASE_URL}/manage`,
ssl: process.env.DATABASE_SSL === "true",
},
schema,
});
}

Expand Down Expand Up @@ -64,7 +67,7 @@ async function migrateTenantDatabase(ownerId: string): Promise<{
sql`SELECT 1 FROM pg_database WHERE datname = ${databaseName}`,
);

if (checkDb.rowCount === 0) {
if (checkDb.count === 0) {
return { success: true, skipped: true };
}

Expand Down
Loading