Add deployment automation

This commit is contained in:
2026-05-23 17:23:01 -06:00
parent 008ea1677f
commit 894bc028f7
7 changed files with 301 additions and 7 deletions

61
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,61 @@
name: Deploy
on:
push:
branches:
- main
concurrency:
group: deploy-production-main
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Set up Supabase CLI
uses: supabase/setup-cli@v1
with:
version: 2.101.0
- name: Link Supabase project
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_REF: ${{ secrets.SUPABASE_PROJECT_REF }}
SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
run: supabase link --project-ref "$SUPABASE_PROJECT_REF" --password "$SUPABASE_DB_PASSWORD"
- name: Apply pending migrations and seed baseline data
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
run: supabase db push --linked --include-seed
- name: Set up Vercel CLI
run: npm install --global vercel@54.4.1
- name: Deploy to Vercel production
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
run: vercel deploy --prod --yes --token "$VERCEL_TOKEN"

9
.gitignore vendored
View File

@@ -31,12 +31,17 @@ yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*.local
# env files
.env
.env.*
!.env.example
!.env*.example
!.env*.sample
!.env*.template
# vercel
.vercel
supabase/.temp/
# typescript
*.tsbuildinfo

View File

@@ -18,7 +18,7 @@ This repo currently stops at:
- environment variable template
- deployment-ready project structure
This repo does not yet include MVP feature pages, SQL schema, avatar logic, habits, streaks, or shop logic.
This repo does not yet include MVP feature pages, avatar logic, habits, streaks, or shop logic.
## Local setup
@@ -59,11 +59,37 @@ Create a Supabase project, then copy the project URL and anon key from **Project
The starter also supports Supabase's publishable key name, but this repo documents the anon key convention from the project plan.
### Database workflow
- Schema migrations live under `supabase/migrations/`.
- Baseline catalog data lives in `supabase/seed.sql` and is safe to rerun during deploys because it uses idempotent upserts.
- Local CLI config lives in `supabase/config.toml` for `supabase start`, `supabase db reset`, and related commands.
- Production database sync runs from GitHub Actions on `main` using `supabase db push --linked --include-seed` before the app deploy relies on those changes.
## Deploy
1. Push the repo to GitHub.
2. Import the repo into Vercel.
3. Add the same Supabase environment variables in Vercel.
4. Deploy.
This repo includes `.github/workflows/deploy.yml`, which runs on pushes to `main`.
The workflow:
- checks out the repo
- runs `npm ci`, `npm run lint`, and `npm run build`
- links the Supabase CLI to the production project
- applies pending database migrations and reruns `supabase/seed.sql` via `supabase db push --linked --include-seed`
- deploys the app to Vercel production only after the database sync succeeds
### Required GitHub secrets
- `SUPABASE_ACCESS_TOKEN`
- `SUPABASE_PROJECT_REF`
- `SUPABASE_DB_PASSWORD`
- `VERCEL_TOKEN`
- `VERCEL_ORG_ID`
- `VERCEL_PROJECT_ID`
### Required Vercel environment variables
- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_ANON_KEY`
Frontend and backend are both served from this Next.js app. Supabase provides auth and database services.

48
supabase/config.toml Normal file
View File

@@ -0,0 +1,48 @@
project_id = "habit-pet"
[api]
enabled = true
port = 54321
schemas = ["public", "graphql_public"]
extra_search_path = ["public", "extensions"]
max_rows = 1000
[db]
port = 54322
shadow_port = 54320
major_version = 15
[db.seed]
enabled = true
sql_paths = ["./seed.sql"]
[studio]
enabled = true
port = 54323
[inbucket]
enabled = true
port = 54324
[storage]
enabled = true
file_size_limit = "50MiB"
[auth]
enabled = true
site_url = "http://127.0.0.1:3000"
additional_redirect_urls = ["http://127.0.0.1:3000/**", "http://localhost:3000/**"]
jwt_expiry = 3600
enable_refresh_token_rotation = true
refresh_token_reuse_interval = 10
[auth.email]
enable_signup = true
double_confirm_changes = true
enable_confirmations = false
[realtime]
enabled = true
[analytics]
enabled = false

View File

@@ -0,0 +1,76 @@
alter table profiles enable row level security;
alter table habits enable row level security;
alter table habit_logs enable row level security;
alter table daily_entries enable row level security;
alter table avatar_state enable row level security;
alter table shop_items enable row level security;
alter table user_items enable row level security;
alter table habits
add constraint habits_id_user_id_unique unique (id, user_id);
alter table habit_logs
add constraint habit_logs_habit_id_user_id_fkey
foreign key (habit_id, user_id)
references habits (id, user_id)
on delete cascade;
create policy "profiles_select_own"
on profiles
for select
to authenticated
using (id = auth.uid());
create policy "profiles_insert_own"
on profiles
for insert
to authenticated
with check (id = auth.uid());
create policy "profiles_update_own"
on profiles
for update
to authenticated
using (id = auth.uid())
with check (id = auth.uid());
create policy "habits_manage_own"
on habits
for all
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
create policy "habit_logs_manage_own"
on habit_logs
for all
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
create policy "daily_entries_manage_own"
on daily_entries
for all
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
create policy "avatar_state_manage_own"
on avatar_state
for all
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());
create policy "shop_items_read_all"
on shop_items
for select
to anon, authenticated
using (true);
create policy "user_items_manage_own"
on user_items
for all
to authenticated
using (user_id = auth.uid())
with check (user_id = auth.uid());

View File

@@ -0,0 +1,67 @@
create extension if not exists pgcrypto;
create table profiles (
id uuid primary key references auth.users(id) on delete cascade,
display_name text,
focus_topics text[] default '{}',
onboarding_complete boolean default false,
created_at timestamptz default now()
);
create table habits (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete cascade not null,
name text not null,
active boolean default true,
created_at timestamptz default now()
);
create table habit_logs (
id uuid primary key default gen_random_uuid(),
habit_id uuid references habits(id) on delete cascade not null,
user_id uuid references auth.users(id) on delete cascade not null,
completed_on date not null default current_date,
created_at timestamptz default now(),
unique (habit_id, completed_on)
);
create table daily_entries (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) on delete cascade not null,
entry_date date not null default current_date,
mood int check (mood between 1 and 5),
stress int check (stress between 1 and 5),
energy int check (energy between 1 and 5),
sleep_hours numeric,
sleep_quality int check (sleep_quality between 1 and 5),
journal text,
created_at timestamptz default now(),
unique (user_id, entry_date)
);
create table avatar_state (
user_id uuid primary key references auth.users(id) on delete cascade,
avatar_name text default 'Pixel Me',
mood_state text default 'neutral',
health int default 50,
energy int default 50,
coins int default 0,
streak int default 0,
equipped_item text default 'none',
updated_at timestamptz default now()
);
create table shop_items (
id text primary key,
name text not null,
type text not null,
price int not null,
image_path text not null
);
create table user_items (
user_id uuid references auth.users(id) on delete cascade,
item_id text references shop_items(id) on delete cascade,
purchased_at timestamptz default now(),
primary key (user_id, item_id)
);

11
supabase/seed.sql Normal file
View File

@@ -0,0 +1,11 @@
insert into shop_items (id, name, type, price, image_path)
values
('hat_blue', 'Blue Hat', 'accessory', 20, '/items/hat-blue.png'),
('glasses_round', 'Round Glasses', 'accessory', 30, '/items/glasses-round.png'),
('room_sunset', 'Sunset Room', 'background', 40, '/backgrounds/room-sunset.png')
on conflict (id) do update
set
name = excluded.name,
type = excluded.type,
price = excluded.price,
image_path = excluded.image_path;