# Tidyly

> Organize your money. Organize your goals. Organize your life.

Tidyly is a multilingual (English/Polish) personal finance & organization SaaS: expense tracking, budgets, savings goals, subscriptions, a simple planner, and a freemium Stripe subscription model — built with Next.js App Router, TypeScript, Supabase, and Stripe.

This is a real, functional codebase — not a static mockup. Auth, the database, RLS policies, and Stripe are all wired up for real. You need your own Supabase project and Stripe account to run it end-to-end (see below).

---

## 1. Tech stack

- **Frontend:** Next.js 14 (App Router), React 18, TypeScript, Tailwind CSS
- **Backend:** Next.js Route Handlers
- **Database & Auth:** Supabase (PostgreSQL + Supabase Auth)
- **Payments:** Stripe (Checkout + Billing Portal + Webhooks)
- **Forms:** React Hook Form + Zod
- **Charts:** Recharts
- **Icons:** Lucide React

## 2. Project structure

```
src/
  app/
    [locale]/                 # every user-facing route is locale-prefixed (/en/..., /pl/...)
      (marketing)/            # homepage etc.
      (auth)/                 # login, register, forgot/reset password, onboarding
      (app)/                  # authenticated app shell: dashboard, expenses, budget, goals...
      admin/                  # admin-only panel
      privacy|terms|cookies|contact/
    api/
      stripe/{checkout,portal,webhook}/
      account/delete/
  components/
    ui/                       # design system primitives (Button, Card, Input, Modal, ...)
    layout/                   # Sidebar, Topbar, AppShell, marketing nav/footer
    dashboard/ expenses/ goals/ budget/ subscriptions/ settings/ shared/
  lib/
    supabase/                 # browser / server / admin / middleware clients
    i18n/                     # dictionary loader + client provider
    validation/                # Zod schemas
    pricing.ts                # single source of truth for plans & prices
    stripe.ts
  middleware.ts               # locale detection + auth route guards
locales/
  en.json  pl.json
supabase/
  migrations/0001_init.sql    # full schema + Row Level Security policies
  seed.sql                    # demo data
```

## 3. Prerequisites

- Node.js 18.18+ (Next.js 14 requirement)
- A [Supabase](https://supabase.com) project
- A [Stripe](https://stripe.com) account (test mode is fine)

## 4. Setup

### 4.1 Install dependencies

```bash
npm install
```

### 4.2 Configure environment variables

```bash
cp .env.example .env.local
```

Fill in:

| Variable | Where to find it |
|---|---|
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project → Settings → API |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase project → Settings → API |
| `SUPABASE_SERVICE_ROLE_KEY` | Supabase project → Settings → API (⚠️ server-only secret, never expose to the client) |
| `STRIPE_SECRET_KEY` | Stripe dashboard → Developers → API keys |
| `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` | Stripe dashboard → Developers → API keys |
| `STRIPE_WEBHOOK_SECRET` | Generated when you create the webhook endpoint (4.4) |
| `STRIPE_PRICE_PLUS_MONTHLY` / `..._YEARLY` / `STRIPE_PRICE_PRO_MONTHLY` / `..._YEARLY` | Stripe dashboard → Products → create "Tidyly Plus" and "Tidyly Pro" products, each with a monthly and yearly recurring Price, then paste the Price IDs here |
| `NEXT_PUBLIC_SITE_URL` | `http://localhost:3000` locally |

### 4.3 Set up the database

In the Supabase SQL editor, run the migration:

```
supabase/migrations/0001_init.sql
```

This creates every table, indexes, the `handle_new_user` trigger (auto-creates a `profiles` + `user_settings` row on signup), and — critically — **Row Level Security policies** on every user-owned table so a user can only ever read/write their own data. It also revokes client-side write access to the Stripe-related profile columns, so only the webhook handler (using the service role key) can change a user's plan.

If you use the Supabase CLI instead:

```bash
supabase link --project-ref <your-project-ref>
supabase db push
```

### 4.4 Set up the Stripe webhook

Locally, use the Stripe CLI:

```bash
stripe listen --forward-to localhost:3000/api/stripe/webhook
```

This prints a `whsec_...` value — put it in `STRIPE_WEBHOOK_SECRET`.

In production, create a webhook endpoint in the Stripe dashboard pointing to `https://yourdomain.com/api/stripe/webhook`, subscribed to at least: `checkout.session.completed`, `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`, `invoice.payment_failed`.

### 4.5 (Optional) Google OAuth

Enable the Google provider under Supabase → Authentication → Providers, and add your OAuth client ID/secret. The login page already calls `signInWithOAuth({ provider: "google" })` — no code changes needed once the provider is configured.

### 4.6 (Optional) Demo data

Create a user manually in Supabase Studio → Authentication → Add user with email `demo@tidyly.app`, then run `supabase/seed.sql` in the SQL editor to populate sample expenses, a goal, and subscriptions.

### 4.7 Run it

```bash
npm run dev
```

Visit `http://localhost:3000` — you'll be redirected to `/en` or `/pl` based on your browser's language.

## 5. Making a user an admin

The admin panel (`/admin`) is gated on `profiles.is_admin`. To grant access, run in the SQL editor:

```sql
update public.profiles set is_admin = true where email = 'you@example.com';
```

## 6. Security notes

- Every table has RLS enabled; policies restrict `SELECT/INSERT/UPDATE/DELETE` to `auth.uid() = user_id`.
- The **backend/database is the source of truth for subscription status** — the Stripe webhook handler (using the service-role client) is the only writer of `subscription_status`, `subscription_plan`, `stripe_subscription_id`, and `subscription_period_end`; those columns are explicitly revoked from the `authenticated` role in the migration.
- `SUPABASE_SERVICE_ROLE_KEY` and `STRIPE_SECRET_KEY` are read only in server-side code (`src/lib/supabase/server.ts`, `src/lib/stripe.ts`) and are never sent to the browser.
- Account deletion (`/api/account/delete`) re-verifies the caller's session before using the admin client, and only ever deletes that same user.

## 7. What's stubbed / needs your input before production

Per the "don't pretend something is implemented" rule, these are intentionally left as clearly-marked placeholders:

- **Legal pages** (`/privacy`, `/terms`, `/cookies`) contain placeholder copy with a visible banner — have a lawyer draft real ones before launch.
- **Google OAuth** works once you configure the provider in Supabase (4.5) — no code is faked.
- **Email notifications**: the `user_settings` table and notification toggles exist, and the `notifications` table is ready to receive rows, but no email-sending provider (e.g. Resend/Postmark) is wired up — add one in a server action or scheduled function.
- **Recurring/scheduled jobs** (e.g. "your Netflix payment is tomorrow" reminders, month-end budget rollovers) need a scheduler — a Supabase Edge Function on a cron trigger, or a Vercel Cron job calling a new API route, is the natural fit.

## 8. Deploying

The app is designed for Vercel:

1. Push to a Git repo, import into Vercel.
2. Add all the environment variables from `.env.local`.
3. Set `NEXT_PUBLIC_SITE_URL` to your production URL.
4. Point your Stripe webhook at `https://yourdomain.com/api/stripe/webhook`.
