Add deployment automation
This commit is contained in:
61
.github/workflows/deploy.yml
vendored
Normal file
61
.github/workflows/deploy.yml
vendored
Normal 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
9
.gitignore
vendored
@@ -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
|
||||
|
||||
36
README.md
36
README.md
@@ -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
48
supabase/config.toml
Normal 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
|
||||
@@ -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());
|
||||
67
supabase/migrations/20260523_initial_schema.sql
Normal file
67
supabase/migrations/20260523_initial_schema.sql
Normal 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
11
supabase/seed.sql
Normal 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;
|
||||
Reference in New Issue
Block a user