GLOBAL-BRIEF.md
# Northwind — Global Brief
Generated by BriefKit | B2B SaaS (sample)
## 0. PRE-BUILD CHECKLIST
- [x] Stripe account created (live + test mode)
- [x] Supabase project created (region: us-east-1)
- [x] Stripe secret key added to Supabase Edge Function Secrets as STRIPE_SECRET_KEY
- [x] Google OAuth client created (authorized redirect: https://northwind.app/auth/callback)
- [x] Domain purchased: northwind.app
- [x] Policy pages drafted (Privacy, Terms, Refund)
## 1. PRODUCT
**What it does:** Northwind is an inventory management SaaS for indie Shopify sellers running 1–3 stores. It syncs stock across stores in real time, warns before oversells, and auto-generates restock POs from sales velocity.
**Ideal customer:** Solo or 2-person Shopify operators doing $20K–$200K/mo GMV, selling physical goods across 1–3 stores, currently tracking stock in spreadsheets.
**Core actions:** Connect Shopify store · View unified inventory · Set low-stock thresholds · Generate restock PO · Export PO as CSV/PDF for supplier.
## 2. MONETIZATION
**Model:** Subscription
**Free trial:** 14 days
**Tiers:**
1. **Starter** — $29/mo (1 store, 500 SKUs)
2. **Growth** — $79/mo (3 stores, 5K SKUs, PO automation)
3. **Scale** — $199/mo (unlimited stores, 50K SKUs, supplier portal)
## 3. CUSTOMER JOURNEY
- **Discover:** Shopify App Store search "inventory sync" + r/shopify recommendations
- **Convince to sign up:** Landing page hero shows live "oversell prevented" counter + 14-day free trial with no card
- **First aha moment:** Connect first store → within 30s see SKU list with photos pulled from Shopify, plus a red badge on the first item already below threshold
- **What makes them stay:** Restock POs that take 2 minutes instead of an afternoon in spreadsheets; one-click resend to supplier
## 4. POSITIONING
**Competitors:** Stocky (Shopify), Cin7, Katana, DEAR
**Differentiator:** Built for 1–3 store operators, not warehouses. No 30-day onboarding, no per-seat pricing, no MRP module nobody uses.
**Reference:** linear.app (clarity), plain.com (calm density)
## 5. VOICE
**Tones:** Direct, Friendly, Confident (no jargon, no "synergize")
## 6. ACCESS CONTROL
**Roles:** Owner, Admin, Staff, Viewer
**Super admin:** enabled (internal Northwind support only)
### Permission matrix
| Action | Owner | Admin | Staff | Viewer |
|------------------------------|:-----:|:-----:|:-----:|:------:|
| View inventory | ✓ | ✓ | ✓ | ✓ |
| Adjust stock counts | ✓ | ✓ | ✓ | |
| Create / send PO | ✓ | ✓ | | |
| Connect / disconnect store | ✓ | ✓ | | |
| Invite users | ✓ | ✓ | | |
| Manage billing | ✓ | | | |
| Delete workspace | ✓ | | | |
## 7. INFRASTRUCTURE
**Payments:** Stripe (Checkout + Billing Portal + webhooks for subscription.updated, invoice.paid, customer.subscription.deleted)
**Hosting:** Vercel (frontend) + Supabase (db, auth, edge functions, storage)
**Background jobs:** Supabase pg_cron — `sync_shopify_stock()` every 5 min, `generate_low_stock_alerts()` every 15 min
## 8. DATABASE SCHEMA
```sql
-- workspaces (one per paying customer)
create table public.workspaces (
id uuid primary key default gen_random_uuid(),
name text not null,
owner_id uuid not null references auth.users(id) on delete restrict,
stripe_customer_id text unique,
plan text not null default 'trial' check (plan in ('trial','starter','growth','scale','canceled')),
trial_ends_at timestamptz not null default (now() + interval '14 days'),
created_at timestamptz not null default now()
);
-- workspace_members
create table public.workspace_members (
workspace_id uuid not null references public.workspaces(id) on delete cascade,
user_id uuid not null references auth.users(id) on delete cascade,
role text not null check (role in ('owner','admin','staff','viewer')),
created_at timestamptz not null default now(),
primary key (workspace_id, user_id)
);
-- shopify_stores
create table public.shopify_stores (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references public.workspaces(id) on delete cascade,
shop_domain text not null,
access_token_encrypted text not null,
last_synced_at timestamptz,
created_at timestamptz not null default now(),
unique (workspace_id, shop_domain)
);
-- skus (deduplicated across stores by sku code)
create table public.skus (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references public.workspaces(id) on delete cascade,
sku text not null,
title text not null,
image_url text,
cost_cents integer,
low_stock_threshold integer not null default 5,
created_at timestamptz not null default now(),
unique (workspace_id, sku)
);
-- stock_levels (per sku per store)
create table public.stock_levels (
sku_id uuid not null references public.skus(id) on delete cascade,
store_id uuid not null references public.shopify_stores(id) on delete cascade,
quantity integer not null default 0,
updated_at timestamptz not null default now(),
primary key (sku_id, store_id)
);
-- purchase_orders
create table public.purchase_orders (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references public.workspaces(id) on delete cascade,
supplier_email text,
status text not null default 'draft' check (status in ('draft','sent','received','canceled')),
total_cents integer not null default 0,
created_by uuid not null references auth.users(id),
created_at timestamptz not null default now()
);
-- audit_log (append-only, service_role writes)
create table public.audit_log (
id bigserial primary key,
workspace_id uuid not null,
actor_id uuid,
action text not null,
target text,
payload jsonb,
created_at timestamptz not null default now()
);
```
## 9. ROW-LEVEL SECURITY POLICIES
```sql
alter table public.workspaces enable row level security;
alter table public.workspace_members enable row level security;
alter table public.shopify_stores enable row level security;
alter table public.skus enable row level security;
alter table public.stock_levels enable row level security;
alter table public.purchase_orders enable row level security;
alter table public.audit_log enable row level security;
-- helper: is user a member of workspace?
create or replace function public.is_member(_ws uuid)
returns boolean language sql stable security definer set search_path = public as $$
select exists (
select 1 from public.workspace_members
where workspace_id = _ws and user_id = auth.uid()
)
$$;
create or replace function public.has_ws_role(_ws uuid, _roles text[])
returns boolean language sql stable security definer set search_path = public as $$
select exists (
select 1 from public.workspace_members
where workspace_id = _ws and user_id = auth.uid() and role = any(_roles)
)
$$;
-- workspaces: members can read; only owner can update; nobody can delete via API
create policy "ws_select" on public.workspaces for select to authenticated
using (public.is_member(id));
create policy "ws_update_owner" on public.workspaces for update to authenticated
using (owner_id = auth.uid()) with check (owner_id = auth.uid());
-- skus / stock_levels: members read, staff+ write
create policy "skus_select" on public.skus for select to authenticated
using (public.is_member(workspace_id));
create policy "skus_write" on public.skus for insert to authenticated
with check (public.has_ws_role(workspace_id, array['owner','admin','staff']));
create policy "skus_update" on public.skus for update to authenticated
using (public.has_ws_role(workspace_id, array['owner','admin','staff']));
-- purchase_orders: only owner/admin can create or send
create policy "po_select" on public.purchase_orders for select to authenticated
using (public.is_member(workspace_id));
create policy "po_write" on public.purchase_orders for insert to authenticated
with check (public.has_ws_role(workspace_id, array['owner','admin']));
-- audit_log: read by admins, writes ONLY by service_role
create policy "audit_select" on public.audit_log for select to authenticated
using (public.has_ws_role(workspace_id, array['owner','admin']));
revoke insert, update, delete on public.audit_log from authenticated;
```
## 10. API ENDPOINTS (Supabase Edge Functions)
| Method | Path | Auth | Purpose |
|--------|-------------------------------------|---------------|--------------------------------------|
| POST | /functions/v1/shopify-connect | user JWT | Exchange OAuth code → store access |
| POST | /functions/v1/shopify-sync | service cron | Pull stock for all active stores |
| POST | /functions/v1/po-create | user JWT | Create draft PO from low-stock list |
| POST | /functions/v1/po-send | user JWT | Email PO PDF to supplier |
| POST | /functions/v1/stripe-webhook | Stripe sig | Handle subscription lifecycle |
| POST | /functions/v1/checkout-session | user JWT | Create Stripe Checkout session |
All non-webhook endpoints validate `workspace_id` against `is_member()` before any read/write.
## 11. TEST USERS & TEST DATA
| Email | Password | Role | Workspace |
|-----------------------------|-------------|--------|------------------|
| owner@northwind.test | Test1234! | owner | Acme Goods |
| admin@northwind.test | Test1234! | admin | Acme Goods |
| staff@northwind.test | Test1234! | staff | Acme Goods |
| viewer@northwind.test | Test1234! | viewer | Acme Goods |
| solo@northwind.test | Test1234! | owner | Solo Store (trial expired) |
**Stripe test cards:** 4242 4242 4242 4242 (success) · 4000 0000 0000 0341 (attach fails) · 4000 0000 0000 9995 (insufficient funds).
## SECURITY BASELINE (run immediately after schema creation)
```sql
revoke insert, update, delete on public.audit_log from authenticated;
revoke insert, update, delete on public.workspaces from authenticated;
-- workspace creation goes through edge function only
```
## BUILD ORDER
1. Schema + RLS + security baseline
2. Auth (Google OAuth + magic link) → onboarding creates workspace via edge fn
3. Screens (landing → auth → onboarding → dashboard → settings → admin)
4. Stub Shopify + Stripe during screen builds
5. Wire Shopify OAuth + sync cron
6. Wire Stripe Checkout + webhook
7. Run security scan (target: 0 errors)
8. Deploy to Vercel + Supabase production
This is what every BriefKit pack looks like.
Real schema, real RLS, real test users — generated from your 5-minute brief.
Generate your own brief pack → Start free