🚀 Weekend Action Plan
Day 1 – Foundations
- Set up monorepo with pnpm workspaces
- Create package structure: frontend, admin-dashboard, mobile, api, shared/ui, shared/config
- Push to GitHub, set branch strategy (main, develop, feature/*)
- Install Supabase CLI, run
supabase start, verify dashboard
Day 2 – Database + ORM
- Define schema with Drizzle ORM (organizations, users, properties, projects, tasks, photos)
- Create and apply first migration, seed sample data
- Connect API routes to database, test with seeded data
Day 3 – CI/CD + Deployment
- Set up GitHub Actions for CI (install, lint, test)
- Configure conditional deploys with
paths-filter - Deploy frontend and API to Vercel
- Wire up environment variables (local + staging + prod)
1) Drizzle schema + SQL migration
// packages/api/src/db/schema.ts
import { pgTable, serial, varchar, integer, text, json, timestamp } from "drizzle-orm/pg-core";
export const organizations = pgTable("organizations", {
id: serial("id").primaryKey(),
slug: varchar("slug", { length: 100 }).notNull().unique(),
name: varchar("name", { length: 200 }).notNull(),
owner_id: integer("owner_id").notNull(),
created_at: timestamp("created_at").defaultNow().notNull()
});
// ... other tables here (users, properties, projects, tasks, photos)
-- db/migrations/001_init.sql
CREATE TABLE organizations (
id serial PRIMARY KEY,
slug varchar(100) NOT NULL UNIQUE,
name varchar(200) NOT NULL,
owner_id integer NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL
);
-- ... other tables here (users, properties, projects, tasks, photos)
2) GitHub Actions workflow
# .github/workflows/ci-cd.yml
name: CI / Conditional Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: npm i -g pnpm
- run: pnpm install
- run: pnpm -w run lint || true
- run: pnpm -w run test || true
detect_changes:
needs: test
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
api: ${{ steps.filter.outputs.api }}
admin: ${{ steps.filter.outputs.admin }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
frontend:
- 'packages/frontend/**'
api:
- 'packages/api/**'
admin:
- 'packages/admin-dashboard/**'
deploy:
needs: detect_changes
runs-on: ubuntu-latest
if: needs.detect_changes.outputs.frontend == 'true' || needs.detect_changes.outputs.api == 'true' || needs.detect_changes.outputs.admin == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm install
# Deploy commands (frontend, api, admin-dashboard) with vercel CLI
1) Drizzle schema + SQL migration
// packages/api/src/db/schema.ts
import { pgTable, serial, varchar, integer, text, json, timestamp } from "drizzle-orm/pg-core";
export const organizations = pgTable("organizations", {
id: serial("id").primaryKey(),
slug: varchar("slug", { length: 100 }).notNull().unique(),
name: varchar("name", { length: 200 }).notNull(),
owner_id: integer("owner_id").notNull(),
created_at: timestamp("created_at").defaultNow().notNull()
});
export const users = pgTable("users", {
id: serial("id").primaryKey(),
org_id: integer("org_id").references(() => organizations.id).notNull(),
email: varchar("email", { length: 320 }).notNull().unique(),
name: varchar("name", { length: 200 }),
role: varchar("role", { length: 32 }).default("member").notNull(),
created_at: timestamp("created_at").defaultNow().notNull()
});
export const properties = pgTable("properties", {
id: serial("id").primaryKey(),
org_id: integer("org_id").references(() => organizations.id).notNull(),
address: text("address").notNull(),
metadata: json("metadata"),
created_at: timestamp("created_at").defaultNow().notNull()
});
export const projects = pgTable("projects", {
id: serial("id").primaryKey(),
org_id: integer("org_id").references(() => organizations.id).notNull(),
property_id: integer("property_id").references(() => properties.id),
name: varchar("name", { length: 200 }).notNull(),
created_at: timestamp("created_at").defaultNow().notNull()
});
export const tasks = pgTable("tasks", {
id: serial("id").primaryKey(),
org_id: integer("org_id").references(() => organizations.id).notNull(),
project_id: integer("project_id").references(() => projects.id),
description: text("description").notNull(),
status: varchar("status", { length: 32 }).default("todo").notNull(),
created_at: timestamp("created_at").defaultNow().notNull()
});
export const photos = pgTable("photos", {
id: serial("id").primaryKey(),
org_id: integer("org_id").references(() => organizations.id).notNull(),
project_id: integer("project_id").references(() => projects.id),
storage_path: text("storage_path").notNull(),
exif: json("exif"),
width: integer("width"),
height: integer("height"),
hash: varchar("hash", { length: 128 }),
status: varchar("status", { length: 32 }).default("processing").notNull(),
created_at: timestamp("created_at").defaultNow().notNull()
});
-- db/migrations/001_init.sql
CREATE TABLE organizations (
id serial PRIMARY KEY,
slug varchar(100) NOT NULL UNIQUE,
name varchar(200) NOT NULL,
owner_id integer NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE users (
id serial PRIMARY KEY,
org_id integer NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email varchar(320) NOT NULL UNIQUE,
name varchar(200),
role varchar(32) NOT NULL DEFAULT 'member',
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE properties (
id serial PRIMARY KEY,
org_id integer NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
address text NOT NULL,
metadata jsonb,
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE projects (
id serial PRIMARY KEY,
org_id integer NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
property_id integer REFERENCES properties(id) ON DELETE SET NULL,
name varchar(200) NOT NULL,
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE tasks (
id serial PRIMARY KEY,
org_id integer NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id integer REFERENCES projects(id) ON DELETE SET NULL,
description text NOT NULL,
status varchar(32) NOT NULL DEFAULT 'todo',
created_at timestamptz DEFAULT now() NOT NULL
);
CREATE TABLE photos (
id serial PRIMARY KEY,
org_id integer NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
project_id integer REFERENCES projects(id) ON DELETE SET NULL,
storage_path text NOT NULL,
exif jsonb,
width integer,
height integer,
hash varchar(128),
status varchar(32) NOT NULL DEFAULT 'processing',
created_at timestamptz DEFAULT now() NOT NULL
);
INSERT INTO organizations (slug, name, owner_id) VALUES ('acme-landscaping','Acme Landscaping', 1);
INSERT INTO users (org_id, email, name, role) VALUES (1, 'owner@acme.test', 'Owner', 'owner');
2) GitHub Actions workflow
# .github/workflows/ci-cd.yml
name: CI / Conditional Deploy
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: npm i -g pnpm
- run: pnpm install
- run: pnpm -w run lint || true
- run: pnpm -w run test || true
detect_changes:
needs: test
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.filter.outputs.frontend }}
api: ${{ steps.filter.outputs.api }}
admin: ${{ steps.filter.outputs.admin }}
steps:
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
frontend:
- 'packages/frontend/**'
api:
- 'packages/api/**'
admin:
- 'packages/admin-dashboard/**'
deploy:
needs: detect_changes
runs-on: ubuntu-latest
if: needs.detect_changes.outputs.frontend == 'true' || needs.detect_changes.outputs.api == 'true' || needs.detect_changes.outputs.admin == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm i -g pnpm
- run: pnpm install
- name: Deploy frontend
if: needs.detect_changes.outputs.frontend == 'true'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_FRONTEND_PROJECT_ID }}
run: |
cd packages/frontend
npx vercel --token "$VERCEL_TOKEN" --prod --confirm --scope="$VERCEL_ORG_ID" --project="$VERCEL_PROJECT_ID"
- name: Deploy api
if: needs.detect_changes.outputs.api == 'true'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_API_PROJECT_ID }}
run: |
cd packages/api
npx vercel --token "$VERCEL_TOKEN" --prod --confirm --scope="$VERCEL_ORG_ID" --project="$VERCEL_PROJECT_ID"
- name: Deploy admin
if: needs.detect_changes.outputs.admin == 'true'
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_ADMIN_PROJECT_ID }}
run: |
cd packages/admin-dashboard
npx vercel --token "$VERCEL_TOKEN" --prod --confirm --scope="$VERCEL_ORG_ID" --project="$VERCEL_PROJECT_ID"
Comments
Post a Comment