summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorMaze <167211895+mazeincoding@users.noreply.github.com>2026-01-31 00:20:04 +0100
committerGitHub <noreply@github.com>2026-01-31 00:20:04 +0100
commit7bf0984698e43b9aa2dd879f65951318ed410390 (patch)
treeccf2cd590a47d550152afc07129cb55c71a9ec35 /packages
parent0173db99448d1c4c6415dcf15272e17551b48b04 (diff)
downloadopencut-7bf0984698e43b9aa2dd879f65951318ed410390.tar.gz
codebase overhaul (#697)
Diffstat (limited to 'packages')
-rw-r--r--packages/auth/package.json22
-rw-r--r--packages/auth/src/client.ts8
-rw-r--r--packages/auth/src/index.ts8
-rw-r--r--packages/auth/src/keys.ts20
-rw-r--r--packages/auth/src/server.ts50
-rw-r--r--packages/db/drizzle.config.ts25
-rw-r--r--packages/db/migrations/0000_brainy_saracen.sql62
-rw-r--r--packages/db/migrations/meta/0000_snapshot.json344
-rw-r--r--packages/db/migrations/meta/_journal.json13
-rw-r--r--packages/db/package.json30
-rw-r--r--packages/db/src/index.ts38
-rw-r--r--packages/db/src/keys.ts19
-rw-r--r--packages/db/src/schema.ts70
-rw-r--r--packages/env/package.json21
-rw-r--r--packages/env/src/tools.ts29
-rw-r--r--packages/env/src/types/node-env.d.ts1
-rw-r--r--packages/env/src/web.ts34
-rw-r--r--packages/ui/package.json19
-rw-r--r--packages/ui/src/icons/brand.tsx67
-rw-r--r--packages/ui/src/icons/index.tsx2
-rw-r--r--packages/ui/src/icons/ui.tsx32
-rw-r--r--packages/ui/tsconfig.json20
22 files changed, 225 insertions, 709 deletions
diff --git a/packages/auth/package.json b/packages/auth/package.json
deleted file mode 100644
index f76a2303..00000000
--- a/packages/auth/package.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "@opencut/auth",
- "version": "0.0.0",
- "description": "Authentication package for OpenCut",
- "main": "./src/index.ts",
- "types": "./src/index.ts",
- "exports": {
- ".": "./src/index.ts",
- "./client": "./src/client.ts",
- "./server": "./src/server.ts",
- "./keys": "./src/keys.ts"
- },
- "dependencies": {
- "@opencut/db": "workspace:*",
- "@t3-oss/env-nextjs": "^0.13.8",
- "better-auth": "^1.1.1",
- "zod": "^4.0.5"
- },
- "devDependencies": {
- "@types/node": "^22.10.2"
- }
-}
diff --git a/packages/auth/src/client.ts b/packages/auth/src/client.ts
deleted file mode 100644
index 2676fb0d..00000000
--- a/packages/auth/src/client.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createAuthClient } from "better-auth/react";
-import { keys } from "./keys";
-
-const { NEXT_PUBLIC_BETTER_AUTH_URL } = keys();
-
-export const { signIn, signUp, useSession } = createAuthClient({
- baseURL: NEXT_PUBLIC_BETTER_AUTH_URL,
-});
diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts
deleted file mode 100644
index 1209c39f..00000000
--- a/packages/auth/src/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-// Re-export server auth
-export * from "./server";
-
-// Re-export client auth
-export * from "./client";
-
-// Re-export keys
-export * from "./keys";
diff --git a/packages/auth/src/keys.ts b/packages/auth/src/keys.ts
deleted file mode 100644
index 3ac528f4..00000000
--- a/packages/auth/src/keys.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { createEnv } from "@t3-oss/env-nextjs";
-import { z } from "zod";
-
-export const keys = () =>
- createEnv({
- server: {
- BETTER_AUTH_SECRET: z.string(),
- UPSTASH_REDIS_REST_URL: z.string().url(),
- UPSTASH_REDIS_REST_TOKEN: z.string(),
- },
- client: {
- NEXT_PUBLIC_BETTER_AUTH_URL: z.string().url(),
- },
- runtimeEnv: {
- NEXT_PUBLIC_BETTER_AUTH_URL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
- BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET,
- UPSTASH_REDIS_REST_URL: process.env.UPSTASH_REDIS_REST_URL,
- UPSTASH_REDIS_REST_TOKEN: process.env.UPSTASH_REDIS_REST_TOKEN,
- },
- });
diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts
deleted file mode 100644
index 84b91b39..00000000
--- a/packages/auth/src/server.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import { betterAuth, RateLimit } from "better-auth";
-import { drizzleAdapter } from "better-auth/adapters/drizzle";
-import { db } from "@opencut/db";
-import { keys } from "./keys";
-import { Redis } from "@upstash/redis";
-
-const {
- NEXT_PUBLIC_BETTER_AUTH_URL,
- BETTER_AUTH_SECRET,
- UPSTASH_REDIS_REST_URL,
- UPSTASH_REDIS_REST_TOKEN,
-} = keys();
-
-const redis = new Redis({
- url: UPSTASH_REDIS_REST_URL,
- token: UPSTASH_REDIS_REST_TOKEN,
-});
-
-export const auth = betterAuth({
- database: drizzleAdapter(db, {
- provider: "pg",
- usePlural: true,
- }),
- secret: BETTER_AUTH_SECRET,
- user: {
- deleteUser: {
- enabled: true,
- },
- },
- emailAndPassword: {
- enabled: true,
- },
- rateLimit: {
- storage: "secondary-storage",
- customStorage: {
- get: async (key) => {
- const value = await redis.get(key);
- return value as RateLimit | undefined;
- },
- set: async (key, value) => {
- await redis.set(key, value);
- },
- },
- },
- baseURL: NEXT_PUBLIC_BETTER_AUTH_URL,
- appName: "OpenCut",
- trustedOrigins: ["http://localhost:3000"],
-});
-
-export type Auth = typeof auth;
diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts
deleted file mode 100644
index d661c455..00000000
--- a/packages/db/drizzle.config.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { Config } from "drizzle-kit";
-import * as dotenv from "dotenv";
-import { keys } from "./src/keys";
-
-const { NODE_ENV, DATABASE_URL } = keys();
-
-// Load the right env file based on environment
-if (NODE_ENV === "production") {
- dotenv.config({ path: ".env.production" });
-} else {
- dotenv.config({ path: ".env.local" });
-}
-
-export default {
- schema: "./src/schema.ts",
- dialect: "postgresql",
- migrations: {
- table: "drizzle_migrations",
- },
- dbCredentials: {
- url: DATABASE_URL,
- },
- out: "./migrations",
- strict: NODE_ENV === "production",
-} satisfies Config;
diff --git a/packages/db/migrations/0000_brainy_saracen.sql b/packages/db/migrations/0000_brainy_saracen.sql
deleted file mode 100644
index 2c6ab2b2..00000000
--- a/packages/db/migrations/0000_brainy_saracen.sql
+++ /dev/null
@@ -1,62 +0,0 @@
-CREATE TABLE "accounts" (
- "id" text PRIMARY KEY NOT NULL,
- "account_id" text NOT NULL,
- "provider_id" text NOT NULL,
- "user_id" text NOT NULL,
- "access_token" text,
- "refresh_token" text,
- "id_token" text,
- "access_token_expires_at" timestamp,
- "refresh_token_expires_at" timestamp,
- "scope" text,
- "password" text,
- "created_at" timestamp NOT NULL,
- "updated_at" timestamp NOT NULL
-);
---> statement-breakpoint
-ALTER TABLE "accounts" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-CREATE TABLE "sessions" (
- "id" text PRIMARY KEY NOT NULL,
- "expires_at" timestamp NOT NULL,
- "token" text NOT NULL,
- "created_at" timestamp NOT NULL,
- "updated_at" timestamp NOT NULL,
- "ip_address" text,
- "user_agent" text,
- "user_id" text NOT NULL,
- CONSTRAINT "sessions_token_unique" UNIQUE("token")
-);
---> statement-breakpoint
-ALTER TABLE "sessions" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-CREATE TABLE "users" (
- "id" text PRIMARY KEY NOT NULL,
- "name" text NOT NULL,
- "email" text NOT NULL,
- "email_verified" boolean NOT NULL,
- "image" text,
- "created_at" timestamp NOT NULL,
- "updated_at" timestamp NOT NULL,
- CONSTRAINT "users_email_unique" UNIQUE("email")
-);
---> statement-breakpoint
-ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-CREATE TABLE "verifications" (
- "id" text PRIMARY KEY NOT NULL,
- "identifier" text NOT NULL,
- "value" text NOT NULL,
- "expires_at" timestamp NOT NULL,
- "created_at" timestamp,
- "updated_at" timestamp
-);
---> statement-breakpoint
-ALTER TABLE "verifications" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-CREATE TABLE "waitlist" (
- "id" text PRIMARY KEY NOT NULL,
- "email" text NOT NULL,
- "created_at" timestamp NOT NULL,
- CONSTRAINT "waitlist_email_unique" UNIQUE("email")
-);
---> statement-breakpoint
-ALTER TABLE "waitlist" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file
diff --git a/packages/db/migrations/meta/0000_snapshot.json b/packages/db/migrations/meta/0000_snapshot.json
deleted file mode 100644
index 8d505274..00000000
--- a/packages/db/migrations/meta/0000_snapshot.json
+++ /dev/null
@@ -1,344 +0,0 @@
-{
- "id": "33a6742f-89da-4ac5-958f-421aa1cf9bd6",
- "prevId": "00000000-0000-0000-0000-000000000000",
- "version": "7",
- "dialect": "postgresql",
- "tables": {
- "public.accounts": {
- "name": "accounts",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "account_id": {
- "name": "account_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "provider_id": {
- "name": "provider_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "access_token": {
- "name": "access_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token": {
- "name": "refresh_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "id_token": {
- "name": "id_token",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "access_token_expires_at": {
- "name": "access_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "refresh_token_expires_at": {
- "name": "refresh_token_expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "scope": {
- "name": "scope",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "password": {
- "name": "password",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "accounts_user_id_users_id_fk": {
- "name": "accounts_user_id_users_id_fk",
- "tableFrom": "accounts",
- "tableTo": "users",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.sessions": {
- "name": "sessions",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "token": {
- "name": "token",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "ip_address": {
- "name": "ip_address",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_agent": {
- "name": "user_agent",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "user_id": {
- "name": "user_id",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {
- "sessions_user_id_users_id_fk": {
- "name": "sessions_user_id_users_id_fk",
- "tableFrom": "sessions",
- "tableTo": "users",
- "columnsFrom": ["user_id"],
- "columnsTo": ["id"],
- "onDelete": "cascade",
- "onUpdate": "no action"
- }
- },
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "sessions_token_unique": {
- "name": "sessions_token_unique",
- "nullsNotDistinct": false,
- "columns": ["token"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.users": {
- "name": "users",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "name": {
- "name": "name",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "email_verified": {
- "name": "email_verified",
- "type": "boolean",
- "primaryKey": false,
- "notNull": true
- },
- "image": {
- "name": "image",
- "type": "text",
- "primaryKey": false,
- "notNull": false
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "users_email_unique": {
- "name": "users_email_unique",
- "nullsNotDistinct": false,
- "columns": ["email"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.verifications": {
- "name": "verifications",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "identifier": {
- "name": "identifier",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "value": {
- "name": "value",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "expires_at": {
- "name": "expires_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- },
- "updated_at": {
- "name": "updated_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": false
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {},
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- },
- "public.waitlist": {
- "name": "waitlist",
- "schema": "",
- "columns": {
- "id": {
- "name": "id",
- "type": "text",
- "primaryKey": true,
- "notNull": true
- },
- "email": {
- "name": "email",
- "type": "text",
- "primaryKey": false,
- "notNull": true
- },
- "created_at": {
- "name": "created_at",
- "type": "timestamp",
- "primaryKey": false,
- "notNull": true
- }
- },
- "indexes": {},
- "foreignKeys": {},
- "compositePrimaryKeys": {},
- "uniqueConstraints": {
- "waitlist_email_unique": {
- "name": "waitlist_email_unique",
- "nullsNotDistinct": false,
- "columns": ["email"]
- }
- },
- "policies": {},
- "checkConstraints": {},
- "isRLSEnabled": true
- }
- },
- "enums": {},
- "schemas": {},
- "sequences": {},
- "roles": {},
- "policies": {},
- "views": {},
- "_meta": {
- "columns": {},
- "schemas": {},
- "tables": {}
- }
-}
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
deleted file mode 100644
index c7d30f16..00000000
--- a/packages/db/migrations/meta/_journal.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "version": "7",
- "dialect": "postgresql",
- "entries": [
- {
- "idx": 0,
- "version": "7",
- "when": 1750753385927,
- "tag": "0000_brainy_saracen",
- "breakpoints": true
- }
- ]
-}
diff --git a/packages/db/package.json b/packages/db/package.json
deleted file mode 100644
index aebbbc90..00000000
--- a/packages/db/package.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "@opencut/db",
- "version": "0.0.0",
- "description": "Database package for OpenCut",
- "main": "./src/index.ts",
- "types": "./src/index.ts",
- "exports": {
- ".": "./src/index.ts",
- "./schema": "./src/schema.ts",
- "./drizzle.config": "./drizzle.config.ts",
- "./keys": "./src/keys.ts"
- },
- "scripts": {
- "db:generate": "drizzle-kit generate",
- "db:migrate": "drizzle-kit migrate",
- "db:push": "drizzle-kit push",
- "db:studio": "drizzle-kit studio"
- },
- "dependencies": {
- "@t3-oss/env-nextjs": "^0.13.8",
- "drizzle-orm": "^0.44.2",
- "postgres": "^3.4.5",
- "zod": "^4.0.5"
- },
- "devDependencies": {
- "drizzle-kit": "^0.31.1",
- "dotenv": "^16.4.7",
- "@types/pg": "^8.11.10"
- }
-}
diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts
deleted file mode 100644
index 9b395ab6..00000000
--- a/packages/db/src/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { drizzle } from "drizzle-orm/postgres-js";
-import postgres from "postgres";
-import * as schema from "./schema";
-import { keys } from "./keys";
-
-const { DATABASE_URL } = keys();
-// Create a lazy database instance that only initializes when accessed
-let _db: ReturnType<typeof drizzle> | null = null;
-
-function getDb() {
- if (!_db) {
- const client = postgres(DATABASE_URL);
- _db = drizzle(client, { schema });
- }
-
- return _db;
-}
-
-// Export a proxy that forwards all calls to the actual db instance
-export const db = getDb();
-
-// Re-export schema for convenience
-export * from "./schema";
-
-// Re-export drizzle-orm functions to ensure version consistency
-export {
- eq,
- and,
- or,
- not,
- isNull,
- isNotNull,
- inArray,
- notInArray,
- exists,
- notExists,
- sql,
-} from "drizzle-orm";
diff --git a/packages/db/src/keys.ts b/packages/db/src/keys.ts
deleted file mode 100644
index 65983416..00000000
--- a/packages/db/src/keys.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createEnv } from "@t3-oss/env-nextjs";
-import { z } from "zod";
-
-export const keys = () =>
- createEnv({
- server: {
- NODE_ENV: z
- .enum(["development", "production", "test"])
- .default("development"),
- DATABASE_URL: z
- .string()
- .startsWith("postgres://")
- .or(z.string().startsWith("postgresql://")),
- },
- runtimeEnv: {
- NODE_ENV: process.env.NODE_ENV,
- DATABASE_URL: process.env.DATABASE_URL,
- },
- });
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
deleted file mode 100644
index a1a69e6d..00000000
--- a/packages/db/src/schema.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
-
-export const users = pgTable("users", {
- id: text("id").primaryKey(),
- name: text("name").notNull(),
- email: text("email").notNull().unique(),
- emailVerified: boolean("email_verified").default(false).notNull(),
- image: text("image"),
- createdAt: timestamp("created_at")
- .$defaultFn(() => /* @__PURE__ */ new Date())
- .notNull(),
- updatedAt: timestamp("updated_at")
- .$defaultFn(() => /* @__PURE__ */ new Date())
- .notNull(),
-}).enableRLS();
-
-export const sessions = pgTable("sessions", {
- id: text("id").primaryKey(),
- expiresAt: timestamp("expires_at").notNull(),
- token: text("token").notNull().unique(),
- createdAt: timestamp("created_at").notNull(),
- updatedAt: timestamp("updated_at").notNull(),
- ipAddress: text("ip_address"),
- userAgent: text("user_agent"),
- userId: text("user_id")
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
-}).enableRLS();
-
-export const accounts = pgTable("accounts", {
- id: text("id").primaryKey(),
- accountId: text("account_id").notNull(),
- providerId: text("provider_id").notNull(),
- userId: text("user_id")
- .notNull()
- .references(() => users.id, { onDelete: "cascade" }),
- accessToken: text("access_token"),
- refreshToken: text("refresh_token"),
- idToken: text("id_token"),
- accessTokenExpiresAt: timestamp("access_token_expires_at"),
- refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
- scope: text("scope"),
- password: text("password"),
- createdAt: timestamp("created_at").notNull(),
- updatedAt: timestamp("updated_at").notNull(),
-}).enableRLS();
-
-export const verifications = pgTable("verifications", {
- id: text("id").primaryKey(),
- identifier: text("identifier").notNull(),
- value: text("value").notNull(),
- expiresAt: timestamp("expires_at").notNull(),
- createdAt: timestamp("created_at").$defaultFn(
- () => /* @__PURE__ */ new Date()
- ),
- updatedAt: timestamp("updated_at").$defaultFn(
- () => /* @__PURE__ */ new Date()
- ),
-}).enableRLS();
-
-export const exportWaitlist = pgTable("export_waitlist", {
- id: text("id").primaryKey(),
- email: text("email").notNull().unique(),
- createdAt: timestamp("created_at")
- .$defaultFn(() => /* @__PURE__ */ new Date())
- .notNull(),
- updatedAt: timestamp("updated_at")
- .$defaultFn(() => /* @__PURE__ */ new Date())
- .notNull(),
-}).enableRLS();
diff --git a/packages/env/package.json b/packages/env/package.json
new file mode 100644
index 00000000..0b794ef4
--- /dev/null
+++ b/packages/env/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@opencut/env",
+ "version": "0.0.0",
+ "description": "Environment package for OpenCut",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts",
+ "./web": "./src/web.ts",
+ "./tools": "./src/tools.ts"
+ },
+ "dependencies": {
+ "zod": "^4.0.5"
+ },
+ "devDependencies": {
+ "dotenv": "^16.4.7",
+ "@types/bun": "latest",
+ "@types/node": "^24.2.1",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/packages/env/src/tools.ts b/packages/env/src/tools.ts
new file mode 100644
index 00000000..625e3d71
--- /dev/null
+++ b/packages/env/src/tools.ts
@@ -0,0 +1,29 @@
+import { z } from "zod";
+
+const toolsEnvSchema = z.object({
+ // Node
+ NODE_ENV: z.enum(["development", "production", "test"]),
+ ANALYZE: z.string().optional(),
+ NEXT_RUNTIME: z.enum(["nodejs", "edge"]).optional(),
+
+ // Public
+ NEXT_PUBLIC_SITE_URL: z.url().default("http://localhost:3000"),
+
+ // Server
+ DATABASE_URL: z
+ .string()
+ .startsWith("postgres://")
+ .or(z.string().startsWith("postgresql://")),
+
+ BETTER_AUTH_SECRET: z.string(),
+ UPSTASH_REDIS_REST_URL: z.url(),
+ UPSTASH_REDIS_REST_TOKEN: z.string(),
+ CLOUDFLARE_ACCOUNT_ID: z.string(),
+ R2_ACCESS_KEY_ID: z.string(),
+ R2_SECRET_ACCESS_KEY: z.string(),
+ R2_BUCKET_NAME: z.string(),
+});
+
+export type ToolsEnv = z.infer<typeof toolsEnvSchema>;
+
+export const toolsEnv = toolsEnvSchema.parse(process.env);
diff --git a/packages/env/src/types/node-env.d.ts b/packages/env/src/types/node-env.d.ts
new file mode 100644
index 00000000..7e3a61cb
--- /dev/null
+++ b/packages/env/src/types/node-env.d.ts
@@ -0,0 +1 @@
+/// <reference types="node" />
diff --git a/packages/env/src/web.ts b/packages/env/src/web.ts
new file mode 100644
index 00000000..4e8b550e
--- /dev/null
+++ b/packages/env/src/web.ts
@@ -0,0 +1,34 @@
+import { z } from "zod";
+
+const webEnvSchema = z.object({
+ // Node
+ NODE_ENV: z.enum(["development", "production", "test"]),
+ ANALYZE: z.string().optional(),
+ NEXT_RUNTIME: z.enum(["nodejs", "edge"]).optional(),
+
+ // Public
+ NEXT_PUBLIC_SITE_URL: z.url().default("http://localhost:3000"),
+ NEXT_PUBLIC_MARBLE_API_URL: z.url(),
+
+ // Server
+ DATABASE_URL: z
+ .string()
+ .startsWith("postgres://")
+ .or(z.string().startsWith("postgresql://")),
+
+ BETTER_AUTH_SECRET: z.string(),
+ UPSTASH_REDIS_REST_URL: z.url(),
+ UPSTASH_REDIS_REST_TOKEN: z.string(),
+ MARBLE_WORKSPACE_KEY: z.string(),
+ FREESOUND_CLIENT_ID: z.string(),
+ FREESOUND_API_KEY: z.string(),
+ CLOUDFLARE_ACCOUNT_ID: z.string(),
+ R2_ACCESS_KEY_ID: z.string(),
+ R2_SECRET_ACCESS_KEY: z.string(),
+ R2_BUCKET_NAME: z.string(),
+ MODAL_TRANSCRIPTION_URL: z.url(),
+});
+
+export type WebEnv = z.infer<typeof webEnvSchema>;
+
+export const webEnv = webEnvSchema.parse(process.env);
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 00000000..81dbae68
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@opencut/ui",
+ "version": "0.0.0",
+ "description": "UI package for OpenCut",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ "./icons": "./src/icons/index.tsx"
+ },
+ "dependencies": {
+ "@iconify/react": "^6.0.2",
+ "@types/react": "^19.2.7",
+ "react": "^19.2.0"
+ },
+ "devDependencies": {
+ "@types/bun": "latest",
+ "typescript": "^5.8.3"
+ }
+}
diff --git a/packages/ui/src/icons/brand.tsx b/packages/ui/src/icons/brand.tsx
new file mode 100644
index 00000000..26aabf86
--- /dev/null
+++ b/packages/ui/src/icons/brand.tsx
@@ -0,0 +1,67 @@
+export function OcVercelIcon({ className }: { className?: string }) {
+ return (
+ <svg
+ className={className}
+ width="20"
+ height="18"
+ viewBox="0 0 76 65"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <title>Vercel</title>
+ <path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="currentColor" />
+ </svg>
+ );
+}
+
+export function OcMarbleIcon({
+ className = "",
+ size = 32,
+}: {
+ className?: string;
+ size?: number;
+}) {
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width={size}
+ height={size}
+ className={className}
+ fill="none"
+ viewBox="0 0 256 256"
+ >
+ <title>Marble</title>
+ <path fill="#202027" d="M0 0h256v256H0z" />
+ <path
+ fill="#fff"
+ d="M116.032 94.016q1.408.256 6.272 9.856 4.856 9.472 11.904 24.576l4.096 8.576a42.4 42.4 0 0 0 4.992-6.784q2.304-3.96 5.76-10.624 3.96-7.56 5.504-9.728 6.144-9.344 13.312-19.84.896-1.408 3.2-1.92a4.8 4.8 0 0 1 1.28-.128q3.072 0 6.784 2.56 3.84 2.56 4.992 5.248a4.8 4.8 0 0 1 .256 1.92q0 1.28-.128 1.92l-.64 2.944q-.256 1.664-.512 3.968a3200 3200 0 0 0-5.888 46.848 56 56 0 0 0-.512 8.064l-.256 4.096a32 32 0 0 0-.128 2.944q0 1.672-.384 2.304-.384.64-1.408.64-.768 0-2.304-.384-4.096-.896-6.272-3.2-2.176-2.432-2.432-6.656-.128-2.176-.128-6.784 0-4.736.256-14.464l.128-7.04.256-10.112q-1.152 1.024-5.888 8.064a328 328 0 0 0-9.088 14.464q-4.48 7.56-5.888 11.648-1.28 3.2-2.56 4.736-1.152 1.536-2.816 1.536-1.536 0-3.968-1.408-6.912-4.224-8.448-11.648a336 336 0 0 0-6.016-23.68 40 40 0 0 0-2.56-6.272q-1.792-3.968-2.304-4.992l-1.664-3.328q-18.68 24.832-25.344 45.568-1.28 4.352-2.432 6.272-1.152 1.792-2.688 1.792-2.432 0-6.912-4.352Q72 158.144 72 154.304q0-2.04.768-4.096a108 108 0 0 1 16-30.08l3.968-5.248a432 432 0 0 0 12.16-16.64q3.072-4.48 8.192-4.48.896 0 2.944.256"
+ />
+ </svg>
+ );
+}
+
+export function OcDataBuddyIcon({
+ className = "",
+ size = 32,
+}: {
+ className?: string;
+ size?: number;
+}) {
+ return (
+ <svg
+ xmlns="http://www.w3.org/2000/svg"
+ width={size}
+ height={size}
+ className={className}
+ viewBox="0 0 8 8"
+ shapeRendering="crispEdges"
+ >
+ <title>Data Buddy</title>
+ <path d="M0 0h8v8H0z" />
+ <path
+ fill="#fff"
+ d="M1 1h1v6H1zm1 0h4v1H2zm4 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zm0 1h1v1H6zM2 6h4v1H2zm1-3h1v1H3zm1 1h1v1H4z"
+ />
+ </svg>
+ );
+}
diff --git a/packages/ui/src/icons/index.tsx b/packages/ui/src/icons/index.tsx
new file mode 100644
index 00000000..2203b633
--- /dev/null
+++ b/packages/ui/src/icons/index.tsx
@@ -0,0 +1,2 @@
+export * from "./brand";
+export * from "./ui";
diff --git a/packages/ui/src/icons/ui.tsx b/packages/ui/src/icons/ui.tsx
new file mode 100644
index 00000000..60231c33
--- /dev/null
+++ b/packages/ui/src/icons/ui.tsx
@@ -0,0 +1,32 @@
+export function OcVideoIcon({
+ className = "",
+ size = 32,
+}: {
+ className?: string;
+ size?: number;
+}) {
+ return (
+ <svg
+ width={size}
+ height={size}
+ className={className}
+ viewBox="0 0 29 24"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <title>Video</title>
+ <path
+ d="M2.41667 11C2.41667 7.70017 2.41667 6.05025 3.65537 5.02513C4.89405 4 6.88771 4 10.875 4H12.0833C16.0706 4 18.0642 4 19.303 5.02513C20.5417 6.05025 20.5417 7.70017 20.5417 11V13C20.5417 16.2998 20.5417 17.9497 19.303 18.9749C18.0642 20 16.0706 20 12.0833 20H10.875C6.88771 20 4.89405 20 3.65537 18.9749C2.41667 17.9497 2.41667 16.2998 2.41667 13V11Z"
+ stroke="currentColor"
+ strokeWidth="1.5"
+ strokeLinecap="round"
+ />
+ <path
+ d="M20.5417 8.90585L20.6938 8.80196C23.2504 7.05623 24.5287 6.18336 25.556 6.60482C26.5833 7.02628 26.5833 8.42355 26.5833 11.2181V12.7819C26.5833 15.5765 26.5833 16.9737 25.556 17.3952C24.5287 17.8166 23.2504 16.9438 20.6938 15.198L20.5417 15.0941"
+ stroke="currentColor"
+ strokeWidth="1.5"
+ strokeLinecap="round"
+ />
+ </svg>
+ );
+}
diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json
new file mode 100644
index 00000000..d179b7d1
--- /dev/null
+++ b/packages/ui/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules"]
+}