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*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files
|
||||||
.env*.local
|
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env*.example
|
||||||
|
!.env*.sample
|
||||||
|
!.env*.template
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
supabase/.temp/
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -18,7 +18,7 @@ This repo currently stops at:
|
|||||||
- environment variable template
|
- environment variable template
|
||||||
- deployment-ready project structure
|
- 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
|
## 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.
|
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
|
## Deploy
|
||||||
|
|
||||||
1. Push the repo to GitHub.
|
This repo includes `.github/workflows/deploy.yml`, which runs on pushes to `main`.
|
||||||
2. Import the repo into Vercel.
|
|
||||||
3. Add the same Supabase environment variables in Vercel.
|
The workflow:
|
||||||
4. Deploy.
|
|
||||||
|
- 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.
|
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