The 14 documents your AI needs to build your app
Getting the AI to write code is the easy part. The hard part is explaining how your product has to work, how it is built and how to know it works. These are the documents that solve it, with a full example of each one.
The prompts to create the whole docs/ folder and keep it up to date.
If you build with AI, getting it to write code is the easy part. The hard part is giving it enough context to understand how your product has to work, how it is built and how to know it really works. Whatever you do not tell it, it makes up.
That is what these documents are for. There are fourteen, each one answers a question no other one answers, and they all describe the same made-up project so you can see how they fit together: Turnio, a booking app for hair salons.
How they are organised
AGENTS.md goes in the root, because that is what agents look for when they open a project. The rest lives in docs/. They come in three groups: what you build, how it is built and how the work is done.
turnio/
├── AGENTS.md ← the agent starts here
├── CLAUDE.md ← one line: @AGENTS.md
└── docs/
├── PRD.md what and for whom
├── APP_FLOW.md where each user goes
├── DESIGN_SYSTEM.md how it looks
├── TRD.md how it works inside
├── DATABASE.md where the data lives
├── API.md how the pieces talk
├── SECURITY.md what must not break
├── CODE_STYLE.md how it is written
├── IMPLEMENTATION_PLAN.md in what order
├── TESTING.md how you know it works
├── DEPLOYMENT.md how it reaches production
├── DECISIONS.md why it is this way
└── PROGRESS.md where we left off| Document | Answers | Written |
|---|---|---|
| PRD.md | What are we building and for whom? | Before anything else |
| APP_FLOW.md | What does the user do, screen by screen? | After the PRD |
| DESIGN_SYSTEM.md | How does it look and move? | Before the first screen |
| TRD.md | What is it built with, and how? | Before the first line |
| DATABASE.md | Where does the data live and who sees it? | Before the first table |
| API.md | How is it requested and returned? | Before the first endpoint |
| SECURITY.md | What must never break? | With the TRD |
| CODE_STYLE.md | How is code written here? | With the first code |
| AGENTS.md | Where do I start and what do I not touch? | With the first code |
| IMPLEMENTATION_PLAN.md | In what order? | Before the first task |
| TESTING.md | How do I know it works? | With the first task |
| DEPLOYMENT.md | How does it reach production? | Before the first deploy |
| DECISIONS.md | Why is it this way? | Every time something is decided |
| PROGRESS.md | Where did we leave off? | At the end of every session |
You do not need all fourteen on day one. A landing page gets by with PRD, DESIGN_SYSTEM and AGENTS. An MVP adds APP_FLOW, TRD, DATABASE, IMPLEMENTATION_PLAN and PROGRESS. The rest come once there are users and money involved. What matters is that the AI has clear, ordered context instead of guessing.
In each document's file tree you can click any other one to jump to it.
1. PRD.md: what you build and for whom
It is the document every other one comes from. It tells the AI what problem the product solves, for whom, what goes into the first version and, almost more important, what does not.
Product document
What we are building, for whom and how we will know it worked. Every other document hangs from this one.
01Summary
- Product
- Turnio
- In one sentence
- Online booking for hair salons, no calls or WhatsApp.
- What it is
- A website where the client books an appointment in 30 seconds and the business sees its calendar fill up without answering a single message.
- Version
- MVP 1.0
- Status
- In progress · launch planned for November 15
02Problem
Small hair salons handle appointments over WhatsApp and the phone. They lose about 6 hours a week answering messages, and 15% of appointments end in a no-show without notice.
03Goal
A business gets its first online booking in under 10 minutes from signing up.
04Users
- Business owner: sets up services and opening hours, and collects the deposit.
- Staff member: sees their schedule for the day and marks who showed up.
- End client: books from their phone, without creating an account.
05Main need
“I want my clients to book on their own and, if they don't show up, at least I keep the deposit.”
Marta, 3-chair hair salon in Sabadell
06MVP features
- Public pageturnio.app/your-business
- Online bookingService, slot and done
- CalendarDay and week per staff member
- DepositCard payment when booking
- RemindersEmail 24 h before
- MetricsBookings and no-shows
07Priorities
MoSCoW: what goes in no matter what, what goes in if there is time and what stays out.
| Priority | What | Why |
|---|---|---|
| Must have | Booking, calendar, opening hours, deposit | Without this there is no product |
| Should have | Reminders, cancel and reschedule | This is what brings no-shows down |
| Could have | Several staff members, metrics | 1 in 3 businesses asks for it |
| Won't have | Native app, loyalty, POS | After validating the MVP |
08Out of scope
What the AI must not build, even if it looks like a good idea.
- Native app for iOS or Android
- Multiple languages
- Google Calendar sync
- Selling products or vouchers
- Chat between client and business
09Success metrics
- 50 active businesses after 3 months
- 40% of their appointments come through Turnio
- No-shows below 5%
- First booking in under 10 minutes
10Assumptions and risks
- Assumption: the client accepts paying a 5 € deposit
- Assumption: the business shares the link on Instagram
- Risk: Booksy and Fresha already exist; we compete on price and simplicity
- Risk: the owner has no time to set anything up
The most common mistake is not writing “Out of scope”. Without that list, the AI adds whatever seems like a good idea, and within a week you have a chat, a points programme and not a single working booking.
2. APP_FLOW.md: where each user goes
It turns the PRD into journeys: which screens exist, in what order you go through them and what the user sees on each one. It stops the AI from inventing screens or forgetting the way back.
App flow
How each user moves through the product: which screens they go through, what they see on each one and what happens when something goes wrong.
01Summary
- Platform
- Responsive web, designed mobile first
- Roles
- Client (no account) · Owner · Staff member
- Client entry point
- The business link in its Instagram bio
- Business entry point
- turnio.app → Create account
02Client flow
- 1BusinessOpens the page from Instagram
- 2ServicePicks a cut, colour or styling
- 3SlotFree day and time
- 4DetailsName, phone and a 5 € deposit
- 5ConfirmedEmail with the appointment
03Key screens
- Marta Hair SalonSabadell · ★ 4.9Women's cut · 25 €Colour · 45 €1BusinessPhoto, address and services
- What can we do for you?Women's cut · 45 minCut and colour · 2 hStyling · 30 min2ServicePrice and duration in plain sight
- Tuesday 1410:0011:3017:003SlotOnly free slots are shown
- Your detailsNamePhoneCard · 5 € deposit4DetailsNo account or password
- Done, Laura!Tuesday 14 · 11:30Women's cut with Marta5ConfirmedReschedule or cancel, one click away
04Business flow
- 1Sign upEmail or Google
- 2ServicesName, price and duration
- 3Opening hoursDays, hours and breaks
- 4ShareLink for the bio
- 5CalendarBookings come in on their own
05Screen map
Every route in the app and what is on it.
/ landing /[negocio] public page /[negocio]/reservar service and slot /reserva/[id] reschedule or cancel /entrar /registro /panel today's calendar /panel/servicios /panel/horario /panel/clientes /panel/ajustes payments and team
06States of each screen
Empty, loading and error, written down before coding them.
| Screen | Empty | Error |
|---|---|---|
| Calendar | “Share your link to get the first one” | Retry without losing the chosen day |
| Slots | “No slots left this week” + next week | Message and back to the service |
| Clients | “They will show up with their first booking” | Retry |
07Alternative paths
- Another client takes the slot while you pay: you go back to the calendar with a notice
- The payment fails: the slot is held for 10 minutes
- You cancel with less than 24 h notice: you lose the deposit, and you are told beforehand
- The business closes on a day with appointments: every client is notified
08Notifications sent
| When | To whom | Channel |
|---|---|---|
| Booking made | Client and business | |
| 24 h before | Client | |
| Cancellation | Business | Email and dashboard |
| No-show | Business | Dashboard |
What almost nobody writes are the empty and error states and the alternative paths. They are exactly what the AI does not imagine on its own, and the first thing a real user runs into.
3. DESIGN_SYSTEM.md: how it looks and how it moves
Colours, typography, spacing, components and motion, with exact values. With it, every new screen looks like the previous ones; without it, each one debuts a different grey and a different radius.
Design system
The colours, type, spacing, components and motion that exist. If it is not here, the AI does not make it up.
01Identity
- Personality
- Warm, clear and calm
- It looks like
- A well-made paper planner
- Never
- Purple gradients, emojis in the interface, hard shadows
02Colours
Only these. No loose hex values in the code.
- Ink#16151A
- Coral#FF6B4A
- Soft coral#FFE8E2
- Background#FAFAF7
- Border#E7E5E0
- Success#2F9E55
- Warning#E6A100
- Error#D93C3C
03Typography
Inter across the whole app. Nothing below 12 px.
| Style | Size | Line | Weight |
|---|---|---|---|
| Heading 1 | 40 | 44 | 700 |
| Heading 2 | 28 | 34 | 600 |
| Heading 3 | 20 | 28 | 600 |
| Body | 16 | 24 | 400 |
| Small | 14 | 20 | 400 |
| Label | 12 | 16 | 500 |
04Spacing
4 px scale. If a gap is not here, it is wrong.
- 4
- 8
- 12
- 16
- 24
- 32
- 48
05Components
Taken from shadcn/ui and adapted here, not on each screen.
| Component | Variants | Rule |
|---|---|---|
| Button | Primary, secondary, ghost, danger | Only one primary per screen |
| Field | Default, error, disabled | Label always visible, never just a placeholder |
| Slot picker | Free, selected, taken | Taken slots are hidden, not struck through |
| Appointment card | Pending, confirmed, no-show | Status in text, not just colour |
| Modal | Confirm, form | Only for what cannot be undone |
| Toast | Success, error | Top on desktop, bottom on mobile, 4 s |
06Radii and shadows
| Token | Value | Where |
|---|---|---|
| radius-sm | 6 px | Fields, tags |
| radius-md | 10 px | Buttons, cards |
| radius-lg | 16 px | Modals, sheets |
| shadow-card | 0 1px 2px / 6% | Cards |
| shadow-pop | 0 12px 32px / 12% | Menus and modals |
07Motion
- Fast · 150 ms
- Hover, press, swap an icon
- Normal · 250 ms
- Menus, tabs, opening a modal
- Slow · 400 ms
- Panels and entering a screen
- Curve
- cubic-bezier(.22, 1, .36, 1), no bounce
- Always
- Respect prefers-reduced-motion
08Accessibility
- Minimum 4.5:1 contrast on text
- Visible focus on everything you can press
- 44 × 44 px touch targets
- Every field with its label
- Nothing is conveyed by colour alone
09Icons and screens
- Icons
- Lucide, 20 px, 1.5 stroke
- Mobile
- Up to 640 px · designed first
- Tablet
- 640 – 1024 px
- Desktop
- From 1024 px · content at 1200 px max
The palette is not enough: write the rule for each component (“only one primary button per screen”) and how long animations last. And if you also want it not to look AI-built, here are six things that fix it.
4. TRD.md: how it works inside
The technical document: stack, architecture, folder structure and requirements with numbers. If the PRD says what, the TRD says with what and how.
Technical document
How it is built: the stack, how the pieces fit together, the external services and the technical limits that are not up for debate.
01Stack
- Next.js 15Web and API
- TypeScriptStrict
- TailwindWith shadcn/ui
- SupabasePostgres, login and files
- StripeDeposit and subscription
- ResendEmails
- VercelHosting and cron
- PostHogAnalytics
- SentryErrors
02Architecture
A single Next.js project. No separate server until one is needed.
- 1BrowserMobile or desktop
- 2Next.jsServer-rendered pages and /api routes
- 3SupabaseData with RLS and login
- 4StripePayment and webhook back
- 5ResendConfirmation and reminder
03Structure
turnio/ ├─ app/ │ ├─ (publico)/[negocio]/ # page and booking │ ├─ (panel)/panel/ # private area │ └─ api/v1/ # API routes ├─ components/ │ ├─ ui/ # adapted shadcn │ └─ booking/ # booking pieces ├─ lib/ # logic, no React ├─ supabase/migrations/ └─ docs/
04A booking, from the inside
- The page asks /api/v1/slots for the free slots
- When one is picked, the slot is held for 10 minutes in the database
- Stripe Checkout charges the deposit
- The Stripe webhook confirms the booking: never the page
- Resend sends the email, and a daily cron sends the reminders
05Non-functional requirements
| Requirement | Target | How it is measured |
|---|---|---|
| Mobile load | LCP under 2.5 s on 4G | Vercel Speed Insights |
| Booking | Under 60 s from start to finish | PostHog funnel |
| Availability | 99.5% per month | External monitor every minute |
| Scale | 500 businesses and 20,000 bookings a month without touching the architecture | Load test before launch |
| Data | Everything in the EU (Frankfurt) | Supabase and Vercel region |
06If a service goes down
| Service | What happens |
|---|---|
| Stripe | Bookings with a deposit are not possible; the page says so |
| Resend | The email goes to a queue and is retried |
| PostHog | Nothing: it never blocks the app |
| Supabase | Maintenance page |
07Constraints
- No separate server or microservices
- No ORM: supabase-js and generated types
- Infrastructure under 50 € a month
- Nothing that forces a paid plan before we have 20 businesses
Write down the constraints. “No separate server” or “under 50 € a month” are the sentences that stop the AI from building you microservices for a booking app.
5. DATABASE.md: where the data lives
The tables, how they relate, who can read and write each one and how the schema changes. It is the most expensive one to skip: a database mistake is not fixed with a deploy.
Database
The tables, how they relate, who can read what and how the schema changes without breaking production.
01Engine
- Database
- Postgres 16 on Supabase, Frankfurt region
- Access
- supabase-js with types generated from the schema
- Migrations
- Plain SQL in supabase/migrations
- Dates
- Always timestamptz in UTC; shown in the business's time zone
- Money
- Integers in cents, never decimals
02Tables
| Table | Key fields | Relationship |
|---|---|---|
| businesses | slug, name, timezone, deposit_cents | Has services, staff and bookings |
| staff | business_id, user_id, role | Belongs to a business |
| services | business_id, name, minutes, price_cents | Belongs to a business |
| availability | staff_id, weekday, starts_at, ends_at | Each staff member's hours |
| customers | business_id, name, phone, email | One per business, no account |
| bookings | service_id, staff_id, customer_id, period, status | The centre of everything |
| payments | booking_id, stripe_session_id, amount_cents | One per booking |
03Relationships
businesses 1──n services businesses 1──n staff 1──n availability businesses 1──n customers 1──n bookings services 1──n bookings n──1 staff bookings 1──1 payments
04Rules enforced by the database, not the code
- A staff member cannot have two overlapping bookings (exclusion constraint on period)
- status is an enum: held, confirmed, cancelled, no_show
- Prices and deposits greater than or equal to 0
- created_at and updated_at on every table
- Bookings are never deleted: they are cancelled
05Row Level Security
On for every table. Without a policy, nobody sees anything.
| Table | Who reads | Who writes |
|---|---|---|
| businesses | Everyone (only slug, name and address) | Its owner |
| services | Everyone | Business staff |
| bookings | Business staff | Only the server |
| customers | Business staff | Only the server |
| payments | Business owner | Only the webhook |
06Conventions
- Tables in English, plural and snake_case
- uuid primary keys; foreign keys as table_id
- An index on every column used to filter
- No select('*') in production
07Migrations
- One migration per change, with a name that says what it does
- A migration that has already been applied is never edited
- Tested with supabase db reset before pushing
- Delete or rename in two steps: add, migrate data, remove
08Backups
- Recovery
- To any point in the last 7 days
- Export
- Weekly, outside Supabase
- Test
- A backup is restored on the first Monday of every month
09Commands
# Local database with the test data supabase start && supabase db reset # New migration supabase migration new add_no_show_to_bookings # TypeScript types from the schema supabase gen types typescript --local > lib/database.types.ts
Put the rules that must never break in the database, like a staff member not having two appointments at once. A rule that only lives in the code gets skipped by the first endpoint that forgets it.
6. API.md: the contract between the pieces
Every endpoint with its method, who can call it, what it receives, what it returns and how it fails. With it, the AI writes the frontend and the backend without them contradicting each other.
API
Every endpoint: what it receives, what it returns, who can call it and how it fails. They all follow the same rules.
01Basics
- URL
- https://turnio.app/api/v1
- Format
- JSON in and out
- Session
- Supabase cookie; public routes do not require it
- Validation
- One Zod schema per route, before touching anything
- Versions
- /v1 in the URL; anything breaking goes to /v2
02Endpoints
| Method | Route | Who | What it does |
|---|---|---|---|
| GET | /businesses/:slug | Public | Business details and services |
| GET | /slots?service=&date= | Public | Free slots for a day |
| POST | /bookings | Public | Holds the slot and opens the payment |
| PATCH | /bookings/:id | Client with link or staff | Changes the time |
| POST | /bookings/:id/cancel | Client with link or staff | Cancels |
| GET | /panel/bookings?date= | Staff | Calendar for a day |
| POST | /panel/services | Owner | Creates a service |
| POST | /webhooks/stripe | Stripe (signature) | Confirms the payment |
| GET | /cron/reminders | Vercel Cron (secret) | Sends the reminders |
03Request
POST /api/v1/bookings
{ "serviceId": "3f2a…", "staffId": "9b1c…", "startsAt": "2026-10-14T09:30:00Z", "customer": { "name": "Laura Gil", "phone": "+34600111222", "email": "laura@correo.com" } }
04Response
201 Created
{ "data": { "bookingId": "c71d…", "status": "held", "heldUntil": "2026-10-01T10:10:00Z", "checkoutUrl": "https://checkout.stripe.com/…" } }
05Errors
Always { error: { code, message } }. The message can be shown as is.
| Code | HTTP | When |
|---|---|---|
| VALIDATION_ERROR | 422 | The body does not match the schema |
| UNAUTHORIZED | 401 | No session |
| FORBIDDEN | 403 | There is a session, but it is not your business |
| NOT_FOUND | 404 | It does not exist or you cannot see it |
| SLOT_TAKEN | 409 | Someone took the slot first |
| RATE_LIMITED | 429 | Too many requests |
| INTERNAL | 500 | Our fault; it goes to Sentry with its id |
06Rules
- POST /bookings accepts Idempotency-Key: a double click does not create two bookings
- Lists paginated with a cursor, 50 at most
- Dates in ISO 8601 and UTC; money in cents
- Stripe ids and internal fields are never returned
07Limits
- Public routes
- 60 requests per minute per IP
- Create booking
- 5 per hour per IP
- Dashboard
- 300 per minute per user
08Webhooks
- The signature is verified before reading anything
- Respond 200 right away and process afterwards
- Every event.id is stored: if it arrives twice, it is ignored
- Events: checkout.session.completed and charge.refunded
Define errors once and for all. Otherwise every endpoint fails in its own way and the frontend fills up with special cases.
7. SECURITY.md: what must not break
The security rules the code always follows, whoever writes it. The AI does not write insecure code out of malice: it writes it because nobody told it what to protect.
Security
What is protected, from whom, and the rules the code always follows, whether a person or an agent wrote it.
01What we protect
- ClientsName, phone and email
- PaymentsCards never go through us
- BusinessesTheir calendar and their clients
- SecretsStripe, Supabase and Resend keys
02Authentication
- Supabase Auth: magic link and Google
- Session in an httpOnly cookie, never in localStorage
- Attempt limits on login and on links
- The client manages their appointment with a signed link that expires
- Two-factor for owners in v1.1
03Authorisation
- RLS on every table, closed by default
- The business_id comes from the session, never from the request body
- The service_role key only in webhooks and cron
- Every dashboard route checks the resource belongs to your business
04Secrets
- .env out of git; .env.example without values
- NEXT_PUBLIC_ only for what anyone can see
- Different keys in preview and production
- If a key leaks, it is rotated the same day
05Input and output
- Everything that comes in is validated with Zod on the server
- HTML coming from a user is never rendered
- Uploads: images only, 2 MB, type checked
- Generic errors to the outside, details only in Sentry
06Headers
In next.config.ts, for every route.
Content-Security-Policy: default-src 'self'; frame-src https://checkout.stripe.com Strict-Transport-Security: max-age=63072000; includeSubDomains X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=()
07Personal data
- Only what the appointment needs is asked for
- A client is deleted on request within 30 days
- No personal data in logs or analytics
- Servers in the EU, a data processing agreement with every provider
08Dependencies
- npm audit on every PR; high or critical does not get in
- Dependabot on
- Before installing anything: downloads, last commit and licence
09If something happens
- 1DetectSentry alert or report
- 2ContainCut off access
- 3RotateEvery affected key
- 4NotifyThose affected within 72 h
- 5LearnWhat failed and what changes
Write them as rules you can check (“the business_id comes from the session, never from the body”), not as wishes (“the app must be secure”). The full list is in the guide to not getting hacked.
8. CODE_STYLE.md: one way of writing
Names, folders, language and framework rules, errors and commits. It makes code from five different sessions look like the same person wrote it.
Code style
How code is written here, so what the AI writes and what you write look like the same person wrote it.
01Principles
- Simple over clever
- No abstraction until the third use
- Delete before adding
- Names explain the what; comments, the why
- Before writing something, check whether it already exists
02Names
| What | Example |
|---|---|
| Components | SlotPicker.tsx |
| Functions | createBooking() |
| Booleans | isHeld, hasDeposit |
| Constants | HOLD_MINUTES |
| Folders and routes | panel/servicios |
| Types | Booking, BookingStatus |
03Where everything goes
By feature, not by file type. The test sits next to what it tests.
components/booking/SlotPicker.tsx # what you see lib/bookings/create-booking.ts # what decides lib/bookings/create-booking.test.ts # what tests it app/api/v1/bookings/route.ts # only validates and calls lib/
04TypeScript
- strict on
- No any: unknown, then check it
- Database types generated, never by hand
- String unions instead of enum
05React
- Server Components by default
- 'use client' only with state or events
- One component per file
- No useEffect to compute what is already known
06Errors
- lib/ returns { data, error }, it does not throw
- Never an empty catch
- Error messages in a single file
07Formatting
- Formatter
- Prettier, on save
- Rules
- Next's ESLint + import/order
- Line
- 100 characters
- Imports
- Absolute with @/
08Before and after
// ✗ Not like this const d = await fetch('/api/v1/slots?s=' + s).then(r => r.json()) as any // ✓ Like this const { data: slots, error } = await getFreeSlots({ serviceId, date }) if (error) return <SlotsError />
09Commits
feat(bookings): hold the slot for 10 minutes during payment fix(slots): slots were shifted by one hour after the clock change docs(progress): end of the October 29 session
One “not like this, like this” example is worth ten rules. The AI imitates better than it obeys.
9. AGENTS.md: where the agent starts
The only one in the root and the first one read. It does not repeat the others: it says which one to read for each task, how work is done and what is never touched. Claude Code reads CLAUDE.md and the other agents read AGENTS.md; with one line that imports one from the other, both work.
Agent instructions
The first thing any AI touching the project reads: what it is, where everything lives and how work is done here.
01The project
Turnio: online booking for hair salons. Next.js 15 and Supabase, deployed on Vercel. We are building the MVP; today's status is in docs/PROGRESS.md.
02What to read before touching anything
No need to read everything. Read what the task calls for.
| If you are going to… | Read |
|---|---|
| Start any session | PROGRESS.md and IMPLEMENTATION_PLAN.md |
| Build or change a screen | APP_FLOW.md and DESIGN_SYSTEM.md |
| Touch tables or queries | DATABASE.md and SECURITY.md |
| Create or change an endpoint | API.md and SECURITY.md |
| Pick a library or a service | TRD.md and DECISIONS.md |
| Ship something to production | DEPLOYMENT.md and TESTING.md |
03How you work
- 1Get your bearingsRead PROGRESS.md
- 2PlanExplain what you will do and wait for the yes
- 3Small changeOne task, one branch
- 4CheckTests, lint and mobile
- 5Write it downUpdate the docs
04Always
- Reuse what already exists before creating anything
- Follow CODE_STYLE.md even if you would do it differently
- Say which file and line backs what you claim
- If a doc contradicts the code, flag it: do not pick one yourself
05Never
- Install a dependency without asking
- Edit a migration that has already been applied
- Turn off RLS, a test or the linter to make something pass
- Push to main
- Make up an API or an option you have not checked
06Ask before
- Deleting data or files
- Changing the database schema
- Touching login, payments or permissions
- Adding an external service
07Commands
npm run dev # localhost:3000 npm run lint npm run test # Vitest npm run test:e2e # Playwright supabase db reset # clean local database
08Done means
- Builds without warnings
- Tests and lint green
- Tested at 375 px wide
- Docs updated in the same commit
- Session logged in PROGRESS.md
09CLAUDE.md
Claude Code reads CLAUDE.md; Codex, Cursor and the rest read AGENTS.md. One imports the other and nothing is duplicated.
# CLAUDE.md @AGENTS.md
The temptation is to put everything here. If AGENTS.md runs past two screens, the agent skims it. It should direct, not explain.
10. IMPLEMENTATION_PLAN.md: in what order
The project split into phases and numbered tasks, each with what has to happen for it to count as done. The AI does one task at a time, and you know exactly which one.
Implementation plan
The order it gets built in, in small phases that can be tested on their own. The AI does one phase at a time, never the whole project.
01Summary
- Goal
- MVP in production with 10 test hair salons
- Duration
- 6 weeks, from October 6 to November 15
- Pace
- One task = one branch = one PR
- Team
- One person and one agent
02Phases
- 1FoundationsWeek 1
- 2BusinessWeek 2
- 3BookingWeek 3
- 4PaymentsWeek 4
- 5NotificationsWeek 5
- 6LaunchWeek 6
03Phase 1 · Foundations
- 1.1 Project, repository and Vercel
- 1.2 Supabase locally and in the cloud
- 1.3 Base tables and RLS
- 1.4 CI with lint and tests
- Done when: a push deploys a preview
04Phase 2 · Business dashboard
- 2.1 Sign up and login
- 2.2 Create services
- 2.3 Opening hours and breaks
- 2.4 Today's calendar
- Done when: an owner sets up her business on her own
05Phase 3 · Client booking
- 3.1 Public business page
- 3.2 Free slot calculation
- 3.3 Slot picker
- 3.4 Hold the slot for 10 minutes
- Done when: two people cannot take the same slot
06Phase 4 · Payments
- 4.1 Connect Stripe to the business
- 4.2 Deposit checkout
- 4.3 Webhook that confirms
- 4.4 Refund when cancelling in time
- Done when: the test payment confirms the appointment
07Phase 5 · Notifications
- 5.1 Domain verified in Resend
- 5.2 Confirmation email
- 5.3 Reminder 24 h before with cron
- 5.4 Reschedule and cancel from the link
- Done when: the reminder arrives on time
08Phase 6 · Launch
- 6.1 Landing and pricing
- 6.2 Funnel analytics
- 6.3 Security review
- 6.4 10 invited businesses
- Done when: the first real booking comes in
09What blocks what
- Without 1.3 (RLS), nothing that touches data starts
- 3.4 goes before 4.2: you do not charge for a slot that is not held
- 5.1 takes up to 48 h to verify: it is requested in week 1
10How to ask the AI
Read AGENTS.md and PROGRESS.md. Implement only task 3.4 of the plan. Before writing code, tell me which files you will touch and how you will test it.
The mistake is asking “build me the app” in a single message. Each phase has to be testable on its own; otherwise, when something breaks, you do not know which of the forty new things it is in.
11. TESTING.md: how you know it works
Which kind of test for what, the flows tested end to end, the edge cases and when each thing runs. It turns “it seems to work” into something you can check.
How it is tested
What is tested, with what, when, and what it means for something to work. Without this, “it works” means “I looked at it once”.
01Which kind of test for what
| Type | Tool | What it covers | How many |
|---|---|---|---|
| Unit | Vitest | Pure logic: slots, prices, dates | Lots |
| Integration | Vitest and local Supabase | Endpoints, RLS and queries | One per endpoint |
| End to end | Playwright | The flows that make money | Few: 6 |
| Manual | A real phone | What you see and touch | Before every release |
02Tools
- VitestUnit and integration
- PlaywrightReal browser
- Testing LibraryComponents
- Local SupabaseClean database for every test
- axeAccessibility
- GitHub ActionsOn every PR
03End-to-end flows
- Book and pay the deposit
- Cancel in time and get the refund
- Two people on the same slot at once
- The owner creates a service and it shows up on her page
- Log in with a magic link
- Reschedule from the email
04Edge cases that are always tested
- The October and March clock changes
- An appointment that ends at midnight
- The last slot of the day
- Double click on “Confirm”
- Name with accents, emojis or 80 letters
- Slow connection (3G)
05Test cases
| ID | Case | Expected result | Type |
|---|---|---|---|
| T-01 | Ask for slots on a closed day | Empty list, no error | Unit |
| T-02 | Book a slot that is already held | 409 SLOT_TAKEN | Integration |
| T-03 | Business A asks for B's bookings | Empty list because of RLS | Integration |
| T-04 | Repeated Stripe webhook | A single confirmation | Integration |
| T-05 | Payment declined | Slot held and a notice | End to end |
| T-06 | Cancel with less than 24 h notice | Warned and not refunded | End to end |
06Rules
- Every fixed bug leaves a test that reproduces it
- A test is never deleted or skipped to make it pass
- Stripe and Resend are mocked; never the real network
- No test depends on the order of the others
- The CI time zone is set to Europe/Madrid
07When they run
| Moment | What |
|---|---|
| Before every commit | Lint and unit tests for what changed |
| On every PR | Everything, plus end to end |
| Before releasing | Manual pass on mobile |
| After releasing | Smoke booking in production |
08Commands
npm run test # unit and integration npm run test -- --watch # while you code npm run test:e2e # Playwright npm run test:e2e -- --ui # watching it in the browser
Name your product's edge cases. In a booking app they are the clock change and two people grabbing the same slot, and the AI will not test them unless you tell it to.
12. DEPLOYMENT.md: how it reaches production
It is missing from most lists and it is one of the ones that saves the most scares: what is in each environment, which variables are needed, in what order each thing ships and how to undo it.
Deployment
How code reaches production, what is in each environment and how to roll back in two minutes when something goes wrong.
01Environments
| Environment | Where | When it updates | Database |
|---|---|---|---|
| Local | localhost:3000 | On save | Local Supabase with test data |
| Preview | turnio-*.vercel.app | On every PR | Staging project |
| Production | turnio.app | On merge to main | Production project |
02From commit to production
- 1PRBranch from main
- 2CILint and tests
- 3PreviewTested on its URL
- 4MigrationsBefore the code
- 5ProductionMerge to main
- 6SmokeOne test booking
03Environment variables
The names live in .env.example; the values, only in Vercel.
| Variable | What for | Public |
|---|---|---|
| NEXT_PUBLIC_SUPABASE_URL | Connect to Supabase | Yes |
| NEXT_PUBLIC_SUPABASE_ANON_KEY | Public key (protected by RLS) | Yes |
| SUPABASE_SERVICE_ROLE_KEY | Webhooks and cron | No |
| STRIPE_SECRET_KEY | Create payments | No |
| STRIPE_WEBHOOK_SECRET | Verify webhooks | No |
| RESEND_API_KEY | Send emails | No |
| CRON_SECRET | So nobody else can trigger the cron | No |
| SENTRY_DSN | Send errors | Yes |
04Before merging
- CI green
- Tested on the preview, on mobile too
- New variables created in production
- Migrations compatible with the previous code
- Nothing to production on a Friday afternoon
05Migrations in production
- Applied from CI, never by hand from your computer
- Always before the code that needs them
- A backup right before a destructive one
06Rollback
- Code: Instant Rollback on Vercel, one click
- New feature: turn off its flag in PostHog
- Database: a new migration that undoes it; never delete the old one
- Log it in PROGRESS.md with the cause
07Monitoring
| What | With what | Alerts when |
|---|---|---|
| Errors | Sentry | A new error or more than 10 in 5 minutes |
| Downtime | External monitor | The site does not respond for 2 minutes |
| Payments | Stripe | A webhook fails |
| Reminders | Vercel Cron | The cron does not finish |
| Speed | Speed Insights | LCP above 2.5 s |
08Domain and email
- DNS
- Cloudflare, with the domain pointing to Vercel
- SPF, DKIM and DMARC verified for Resend
- HTTPS
- Automatic on Vercel; HSTS on
The rollback plan is written before you need it: at eleven at night with the app down, nobody improvises it well. The thirteen checks before shipping to production complete it.
13. DECISIONS.md: why it is this way
A log of decisions, each with its context, what was ruled out and what it costs. It stops an AI with no memory from undoing in five minutes something that took you a week to decide.
Decision log
Why the project is the way it is. Every decision with its context, what was ruled out and what it costs, so nobody undoes it without knowing. Not even the AI.
01Index
| ADR | Decision | Status | Date |
|---|---|---|---|
| 001 | Next.js with no separate server | Accepted | Oct 2 |
| 002 | Supabase instead of Firebase | Accepted | Oct 2 |
| 003 | SMS reminders | Superseded by 006 | Oct 3 |
| 004 | The client books without an account | Accepted | Oct 6 |
| 005 | The webhook confirms the deposit, not the page | Accepted | Oct 27 |
| 006 | Email reminders; WhatsApp in v2 | Accepted | Oct 28 |
02Template
## ADR-000: Short title Date · Status: proposed | accepted | superseded Context: what the problem was Decision: what was chosen Alternatives: what was ruled out and why Consequences: what we gain and what we pay
03When to write one
- There are two reasonable options and one is chosen
- A dependency or a paid service comes in
- The change is expensive to undo
- Someone will ask “why like this?”
04ADR-004 · The client books without an account
- Context
- In testing, 4 in 10 clients dropped off when they saw “Create your account”.
- Decision
- Booking takes name, phone and email. The appointment is managed with a signed link.
- Alternatives
- Mandatory account (more drop-off) or Google login (not everyone has it).
- Consequences
- More bookings. In exchange, a client may show up twice: they are merged by phone.
- Revisit if
- We add loyalty or a history for the client.
05ADR-005 · The webhook confirms the deposit
- Context
- If the client closes the tab after paying, the success page never loads.
- Decision
- Only checkout.session.completed marks the booking as confirmed.
- Alternatives
- Confirm on return from Stripe: fast, but it loses payments.
- Consequences
- A few seconds of “confirming…” in exchange for never losing a paid booking.
06Rules
- A decision is never deleted: it is superseded by another
- It fits on one screen
- The AI drafts it; you approve it
- If the AI proposes undoing one, it quotes it first
Do not write one for everything. Only when there were two reasonable options or when undoing it is expensive.
14. PROGRESS.md: where we left off
The memory between sessions. The AI does not remember yesterday's conversation, but it can read a file: what is done, what is missing, which branch you are on and what has already failed.
Progress
The project's memory between sessions: where we left off, what is missing and what has gone wrong. The AI reads it when it starts and writes it when it finishes.
01Current status
- Phase
- 3 · Client booking
- Last session
- October 29, 2026
- Branch
- feat/hold-slot
- Next task
- 3.4 Hold the slot for 10 minutes during payment
- Blockers
- None
02Done
- Phases 1 and 2 complete
- 3.1 Public business page
- 3.2 Slot calculation, with 14 tests
- 3.3 Slot picker on mobile
03Pending
- 3.4 Hold the slot (started)
- Review error messages with Marta
- Test the slot picker on an old Android phone
- Debt: the slot picker loads twice
04Known pitfalls
What has already failed once and must not fail again.
| Problem | Cause | Fix |
|---|---|---|
| Slots shifted by one hour | They were calculated in the server's time zone | Everything in UTC, converted when rendering |
| The calendar came up empty with no error | The RLS select policy was missing | An RLS test per table |
| A booking confirmed twice | Stripe retries webhooks | Store event.id |
| E2E green locally and red in CI | CI runs in UTC | TZ=Europe/Madrid in CI |
05Latest sessions
| Date | What was done | Commit |
|---|---|---|
| Oct 29 | Slot picker and its empty and error states | a41c9e2 |
| Oct 28 | Slot calculation with breaks and holidays | 7be0d13 |
| Oct 27 | Public page and ADR-005 | f93a6c1 |
06Before closing the session
- Update the current status and the next task
- Move what is finished to “Done”
- If something failed, log the pitfall and its fix
- If something was decided, write its ADR
- Commit: docs(progress): end of session
The most valuable part is the known pitfalls: every mistake written down is a mistake the agent does not repeat. Close it at the end of every session with the second prompt in the article.
Getting it to read them and keeping them true
An outdated document is worse than none, because the AI believes it. Six rules so that does not happen:
- Each fact in one place. If the deposit price is in the PRD and in the database doc, one day they will say different things. One tells it and the other links to it.
- In the same commit. The code change that contradicts a document fixes it in the same commit, not “later”.
- Short. One or two screens each. Whatever does not help the agent decide is extra.
- Name them in the prompt. “Follow DATABASE.md” works better than waiting for it to find it.
- Numbers instead of adjectives. “Under 2.5 seconds” can be checked; “fast” cannot.
- Review them every month. Ask the agent to compare each document with the code and tell you where they lie.
The three prompts: the first asks you the questions and writes the documents, the second closes each session and the third checks the documentation still tells the truth.