- Home
- Skills
- Agents & Orchestration
- App Blueprint
Works with the AI Agents you already use
App Blueprint
Advanced Application Blueprint
$10
App Blueprint
Example session with this skill installed
Plan an app for dog boarding kennels
- Read your context and instructions
- Compiled the app blueprint
KennelDesk - Application Blueprint
1. PROJECT OVERVIEW
KennelDesk is the booking and daily-operations portal for a single dog boarding kennel (~40 runs, ~10 staff, ~2,000 owner households). Owners book stays, keep their dogs' vaccination proof current, pay a deposit, and follow their dog's stay through daily photo updates. Staff run the floor from one console: who arrives today, which run each dog is in, what it eats, which medication is due, and who still owes a balance.
Target users
- Owners - dog owners who board their dogs; use a phone, sign in by email magic link.
Attendants - kennel floor staff; check dogs in and out, log meals, walks and medication, post daily updates, report incidents.
Managers - kennel owner or office staff; configure runs, prices and policies, review vaccinations, handle cancellations and refunds, invite staff.
Core features
Online stay booking with live run availability - bookingService.createStay reserves a run inside one transaction; a Postgres exclusion constraint on RunAssignment makes double-booking a run impossible.
Deposit payment - Stripe Checkout session per stay; the checkout.session.completed webhook confirms the stay; an unpaid hold expires after 35 minutes through a pg-boss job.
Vaccination proof upload and review - owners upload a photo/PDF straight to object storage with a presigned URL; managers verify or reject it; check-in is refused unless every required vaccine is verified and valid through the checkout date.
Check-in / check-out desk - attendant actions on the stay that enforce vaccine status, record handover times, and compute the balance due at checkout.
Daily care board - server-rendered per-day board of every boarded dog with feeding instructions and medication doses due; doses are logged against a unique (medication, scheduled time) key so two attendants cannot double-dose.
Daily photo updates - attendants post a note and photos per stay per day; a worker job strips EXIF and makes thumbnails; owners get an email and view the update in the portal.
Incident reporting - attendant files an incident (injury, illness, fight, escape attempt) and the owner is emailed immediately.
Cancellation and refunds - owner or manager cancels; the refund follows the kennel's cancellation window from KennelSettings, issued through the Stripe Refunds API; managers may override.
also supports: staff invitations with role assignment, configurable run types and nightly prices, balance payment by Stripe link or recorded in-person payment, audit log of medication, vaccine reviews and refunds, photo retention cleanup.
Architecture: a
modular monolith - one Next.js App Router application with a server-only domain layer (src/server/), plus a separate worker process (same codebase, same image) consuming pg-boss jobs from the same PostgreSQL database. At ~30 concurrent users and ~10 req/sec, one database and one web process have large headroom; splitting services would only add network failure modes. Postgres is the single source of truth for availability, payments and care state, and also hosts the job queue, so no broker or cache is needed.
2. FULL FOLDER STRUCTURE
kenneldesk/
├── .github/workflows/ci.yml # lint, typecheck, unit, integration, e2e, deploy to Fly
├── prisma/ # data model and migrations
│ ├── schema.prisma # Prisma schema (all §4 entities)
│ ├── migrations/ # prisma migrate SQL; exclusion constraint added by hand
│ └── seed.ts # settings row, run types, runs, demo household
├── src/
│ ├── app/ # Next.js App Router: pages and route handlers
│ │ ├── (owner)/ # owner portal: dogs, vaccinations, stays, updates
│ │ ├── (staff)/ # staff console: board, desk, care, settings
│ │ ├── api/ # route handlers (contracts in §5)
│ │ │ ├── auth/[...all]/route.ts # Better Auth handler (sign-in, sessions, 2FA)
│ │ │ ├── webhooks/stripe/route.ts # Stripe event intake, signature-verified
│ │ │ ├── stays/[stayId]/… # stay actions (same pattern per resource)
│ │ │ └── … # dogs, vaccinations, uploads, runs, settings
│ │ ├── layout.tsx # root layout, session-aware nav
│ │ └── error.tsx # error boundary reported to Sentry
│ ├── components/ # shared React UI (forms, board rows, photo grid)
│ ├── server/ # server-only domain code ("server-only" import)
│ │ ├── auth.ts # Better Auth config, role guards requireRole()
│ │ ├── db.ts # Prisma client singleton
│ │ ├── services/ # domain rules: booking, care, vaccination, payment
│ │ │ └── bookingService.ts # availability, holds, run assignment, cancellation
│ │ ├── repos/ # household-scoped queries; only folder importing Prisma
│ │ └── integrations/ # stripe.ts, storage.ts (R2), email.ts (Postmark)
│ ├── jobs/ # pg-boss worker process
│ │ ├── worker.ts # worker entry point (Fly process group "worker")
│ │ └── handlers/ # holdExpiry, photoProcess, vaccineExpiry, retention
│ ├── lib/ # pure helpers, no I/O
│ │ ├── kennelDates.ts # night counting and ranges in the kennel time zone
│ │ └── money.ts # integer-cents pricing and deposit math
│ └── env.ts # zod-validated environment, parsed at boot
├── tests/ # unit/, integration/, e2e/ (Vitest + Playwright)
├── docker-compose.yml # local Postgres 17
├── Dockerfile # one image for web and worker
├── fly.toml # Fly app: web + worker process groups, release_command
├── next.config.ts # Next.js config (output: standalone)
├── vitest.config.ts # unit + integration projects
├── playwright.config.ts # e2e against next start
├── eslint.config.mjs # lint, incl. ban on Prisma imports outside repos/
├── package.json # scripts: dev, build, worker, test, e2e
└── .env.example # documented env vars (§7)
3. LAYER-BY-LAYER BREAKDOWN
.github/workflows/
- What lives there: the single CI/CD workflow.
- Key files and their roles:
ci.yml- lint, typecheck, Vitest unit and integration (Postgres service), Playwright e2e, thenflyctl deployonmain. - How it communicates with adjacent layers: runs the
package.jsonscripts againsttests/; deploys theDockerfileimage usingfly.toml.
prisma/
- What lives there: the schema, the migration history, and seed data.
- Key files and their roles:
schema.prismadefines every §4 entity;migrations/holds generated SQL plus one hand-edited migration that enablesbtree_gistand adds theRunAssignmentexclusion constraint;seed.tscreates theKennelSettingsrow (id 1), run types and runs. - How it communicates with adjacent layers:
prisma generateproduces the client used bysrc/server/db.ts;prisma migrate deployruns as the Fly release command before new machines start.
src/app/
- What lives there: all routes.
(owner)/and(staff)/are route groups for the two portals;api/holds the route handlers of §5. - Key files and their roles:
api/auth/[...all]/route.tsmounts Better Auth;api/webhooks/stripe/route.tsreads the raw body and verifies the signature; each(staff)layout callsrequireRole()before rendering. - How it communicates with adjacent layers: pages (Server Components) and route handlers call
src/server/services/only. They never import Prisma. Client components call route handlers withfetch. Every staff page that shows care or occupancy state exportsdynamic = "force-dynamic".
src/components/
- What lives there: presentational React components shared by both portals: booking form, dog card, vaccine status badge, care board row, photo grid.
- Key files and their roles: one component per file; client components marked
"use client". - How it communicates with adjacent layers: receive data as props from
src/app/pages; submit tosrc/app/api/routes. No server imports.
src/server/
- What lives there: all server-only domain logic, behind the
server-onlypackage so it can never be bundled into a client component. - Key files and their roles:
auth.ts(Better Auth instance with Prisma adapter, magic link, two-factor plugin,requireRole());db.ts(Prisma singleton);services/bookingService.ts(quote, hold, assign run, confirm, cancel, refund);services/careService.ts(board, care logs, incidents);services/vaccinationService.ts(upload, review, check-in eligibility);services/paymentService.ts(Checkout sessions, webhook handling, balances);repos/(queries that take the caller's session and scope byhouseholdIdfor owners);integrations/(Stripe, R2 via the S3 client, Postmark). - How it communicates with adjacent layers: called by
src/app/andsrc/jobs/; calls Postgres throughrepos/, and external APIs throughintegrations/; enqueues jobs through pg-bosssend().
src/jobs/
- What lives there: the background worker, run as its own process (
node dist/jobs/worker.js) in the same image. - Key files and their roles:
worker.tsstarts pg-boss, registers handlers and cron schedules;handlers/holdExpiry.ts(every minute: cancelpending_depositstays pastholdExpiresAt);handlers/photoProcess.ts(sharp: strip EXIF, resize, write thumbnail);handlers/vaccineExpiry.ts(daily: email owners whose vaccine expires before an upcoming stay);handlers/retention.ts(daily: delete photos 90 days after checkout); email send jobs with retry. - How it communicates with adjacent layers: uses
src/server/services/andsrc/server/integrations/; reads and writes Postgres; never serves HTTP.
src/lib/
- What lives there: pure functions with no I/O, unit-tested in isolation.
- Key files and their roles:
kennelDates.ts(calendar-date ranges[checkIn, checkOut), night counts, "today" in the kennel's IANA time zone);money.ts(integer-cents totals, extra-dog pricing, deposit and refund amounts). - How it communicates with adjacent layers: imported by
src/server/services/and by components for display formatting.
tests/
- What lives there:
unit/(Vitest, puresrc/lib/and service rules with fakes),integration/(Vitest against a real Postgres, route handlers invoked withRequestobjects),e2e/(Playwright againstnext start). - Key files and their roles:
integration/setup.tsresets the database per file withprisma migrate reset --force --skip-seed;e2e/fixtures.tssigns in seeded owner and attendant sessions. - How it communicates with adjacent layers: imports
src/directly; e2e drives the built app over HTTP.
4. DATA MODELS AND RELATIONSHIPS
All entities are PostgreSQL tables managed by Prisma; ids are String @id @default(cuid()) unless stated. Money is integer cents. Calendar dates are Postgres date (@db.Date), interpreted in the kennel time zone from KennelSettings.
PortalUser - storage: table (Better Auth user model, renamed via user.modelName) - fields: id:string, email:string (unique), name:string, emailVerified:boolean, role:enum(owner|attendant|manager), twoFactorEnabled:boolean, disabledAt:datetime?
- relations: → Household (1:1, owners only), → Session (1:N), → AuthAccount (1:N) - change: stable.
Session / AuthAccount / Verification - storage: tables owned by Better Auth (session tokens, credential and magic-link records) - fields: as generated by the Better Auth CLI - relations: → PortalUser (N:1) - change: stable (changes only with library upgrades).
StaffInvite - storage: table - fields: id:string, email:string, role:enum(attendant|manager), tokenHash:string (unique), expiresAt:datetime, acceptedAt:datetime?, invitedById:string - relations: → PortalUser (N:1 inviter) - change: stable.
Household - storage: table - fields: id:string, primaryUserId:string (unique), phone:string, addressLine:string, postcode:string, emergencyContactName:string, emergencyContactPhone:string, vetPractice:string, vetPhone:string, stripeCustomerId:string? (unique) - relations: → PortalUser (1:1), → Dog (1:N), → Stay (1:N) - change: evolving (⚠ second authorized pickup person likely within 6 months).
Dog - storage: table - fields: id:string, householdId:string, name:string, breed:string, sex:enum(male|female), neutered:boolean, birthDate:date?, weightKg:decimal(5,2), behaviorNotes:text, archivedAt:datetime?
- relations: → Household (N:1), → Vaccination (1:N), → StayDog (1:N) - change: evolving (⚠ microchip number field likely).
Vaccination - storage: table - fields: id:string, dogId:string, vaccine:enum(rabies|dhpp|bordetella|canine_influenza|leptospirosis), administeredOn:date, expiresOn:date, documentKey:string, status:enum(pending|verified|rejected), reviewedById:string?, reviewedAt:datetime?, rejectReason:string?
- relations: → Dog (N:1), → PortalUser (N:1 reviewer) - change: stable.
RunType - storage: table - fields: id:string, name:string (unique), nightlyRateCents:int, extraDogRateCents:int, maxDogs:int, active:boolean - relations: → Run (1:N) - change: evolving (⚠ peak-season pricing likely).
Run - storage: table - fields: id:string, code:string (unique, e.g. "A07"), runTypeId:string, active:boolean, notes:string?
- relations: → RunType (N:1), → RunAssignment (1:N) - change: stable.
Stay - storage: table - fields: id:string, householdId:string, checkInDate:date, checkOutDate:date, status:enum(pending_deposit|confirmed|checked_in|checked_out|cancelled|no_show), holdExpiresAt:datetime?, totalCents:int, depositCents:int, ownerNotes:text?, cancelledAt:datetime?, cancelReason:string?, cancelledById:string?, checkedInAt:datetime?, checkedOutAt:datetime?, version:int - relations: → Household (N:1), → StayDog (1:N), → RunAssignment (1:N, one active in v1), → DailyUpdate (1:N), → Payment (1:N) - change: volatile. Check constraint checkOutDate > checkInDate.
StayDog - storage: table - fields: id:string, stayId:string, dogId:string, feedingInstructions:text, ownFood:boolean - relations: → Stay (N:1), → Dog (N:1), → StayMedication (1:N), → CareLog (1:N), → Incident (1:N) - change: evolving. Unique (stayId, dogId).
StayMedication - storage: table - fields: id:string, stayDogId:string, drugName:string, dose:string, scheduleTimes:string[] (local "HH:mm"), withFood:boolean, instructions:text?
- relations: → StayDog (N:1), → CareLog (1:N) - change: stable.
RunAssignment - storage: table - fields: id:string, stayId:string, runId:string, nights:daterange (Unsupported("daterange"), [checkInDate, checkOutDate)), active:boolean - relations: → Stay (N:1), → Run (N:1) - change: stable. Constraint: EXCLUDE USING gist (runId WITH =, nights WITH &&) WHERE (active); requires the btree_gist extension.
CareLog - storage: table - fields: id:string, stayDogId:string, kind:enum(meal|medication|walk|potty|note), stayMedicationId:string?, scheduledFor:datetime?, outcome:enum(done|ate_all|ate_some|refused|missed), note:text?, loggedById:string, loggedAt:datetime - relations: → StayDog (N:1), → StayMedication (N:1), → PortalUser (N:1) - change: volatile (append-only). Unique (stayMedicationId, scheduledFor).
Incident - storage: table - fields: id:string, stayDogId:string, severity:enum(minor|vet_required|emergency), description:text, actionTaken:text, reportedById:string, occurredAt:datetime, ownerNotifiedAt:datetime?
- relations: → StayDog (N:1), → PortalUser (N:1) - change: stable.
DailyUpdate - storage: table - fields: id:string, stayId:string, forDate:date, message:text, authorId:string, publishedAt:datetime - relations: → Stay (N:1), → UpdatePhoto (1:N) - change: stable. Unique (stayId, forDate).
UpdatePhoto - storage: table (bytes in R2) - fields: id:string, dailyUpdateId:string, originalKey:string, displayKey:string?, thumbKey:string?, status:enum(uploaded|ready|failed|purged), width:int?, height:int?
- relations: → DailyUpdate (N:1) - change: stable.
Payment - storage: table - fields: id:string, stayId:string, kind:enum(deposit|balance|refund), method:enum(stripe|in_person_card|cash), amountCents:int, status:enum(pending|succeeded|failed), stripeCheckoutSessionId:string? (unique), stripePaymentIntentId:string?, stripeRefundId:string? (unique), recordedById:string?, createdAt:datetime - relations: → Stay (N:1) - change: evolving.
StripeEvent - storage: table - fields: id:string (Stripe event id, primary key), type:string, processedAt:datetime - relations: none - change: stable (webhook idempotency ledger).
KennelSettings - storage: single-row table, id Int @id @default(1) with check constraint id = 1 - fields: id:int, kennelName:string, timeZone:string (IANA), depositPercent:int, holdMinutes:int (default 35), cancellationWindowHours:int, checkInStart:string, checkOutEnd:string, requiredVaccines:enum[], photoRetentionDays:int (default 90) - relations: none - change: evolving.
AuditEvent - storage: table - fields: id:string, actorId:string?, action:string (e.g. "vaccination.verified", "refund.issued", "medication.logged"), subjectType:string, subjectId:string, data:jsonb, createdAt:datetime - relations: → PortalUser (N:1) - change: volatile (append-only).
Migration strategy: schema changes go through prisma migrate dev locally, which writes SQL into prisma/migrations/; the files are reviewed and committed, and production applies them with prisma migrate deploy as the Fly release_command, which runs once per deploy before new machines start. Features Prisma cannot express - btree_gist, the RunAssignment exclusion constraint, the KennelSettings id check, the Stay date check - are added with prisma migrate dev --create-only and edited by hand; the nights column is declared Unsupported("daterange") so Prisma keeps it in the schema (verify that migrate dev reports no drift for the hand-written constraint). Changes are expand-then-contract: add a nullable column, backfill in a later deploy, then tighten.
5. API AND INTERFACE CONTRACTS
| Domain Role | Maps To | Notes |
|---|---|---|
| owner | user | scoped to own Household, its Dogs and Stays |
| attendant | user | staff; all stays, care, updates, incidents; no pricing, refunds or settings; 2FA required |
| manager | admin | everything, incl. settings, runs, vaccine review, refunds, staff invites; 2FA required |
Better Auth routes are (native); all others are (custom) route handlers. Errors return {error: {code, message}} with 400/401/403/404/409. "Owner of X" means the owner's household owns X.
Auth and staff
POST /api/auth/sign-in/magic-linkbody: {email, callbackURL} → 200 {status} auth: [public] (native)POST /api/auth/sign-in/emailbody: {email, password} → 200 {user, twoFactorRedirect?} auth: [public] (native; staff only, followed by/api/auth/two-factor/verify-totp)POST /api/staff-invitesbody: {email, role} → 201 {inviteId, expiresAt} auth: [manager]POST /api/staff-invites/acceptbody: {token, name, password} → 201 {userId, role, totpUri} auth: [public, token-gated]PATCH /api/staff/:userIdbody: {role?, disabled?} → 200 {userId, role, disabledAt} auth: [manager]
Household and dogs
PUT /api/households/mebody: {phone, addressLine, postcode, emergencyContactName, emergencyContactPhone, vetPractice, vetPhone} → 200 {householdId} auth: [owner]POST /api/dogsbody: {name, breed, sex, neutered, birthDate?, weightKg, behaviorNotes} → 201 {dogId} auth: [owner]PATCH /api/dogs/:dogIdbody: {…same fields, archived?} → 200 {dogId} auth: [owner of dog, manager]
Uploads and vaccinations
POST /api/uploads/presignbody: {purpose: "vaccination"|"update_photo", contentType, sizeBytes} → 200 {key, uploadUrl, headers, expiresAt} auth: [owner for vaccination; attendant, manager for update_photo]POST /api/dogs/:dogId/vaccinationsbody: {vaccine, administeredOn, expiresOn, documentKey} → 201 {vaccinationId, status: "pending"} auth: [owner of dog, manager]POST /api/vaccinations/:vaccinationId/reviewbody: {decision: "verified"|"rejected", rejectReason?} → 200 {status} auth: [manager] - emails the owner on rejectionGET /api/vaccinations/:vaccinationId/document→ 302 to a 5-minute presigned GET URL auth: [owner of dog, attendant, manager]
Availability and stays
GET /api/availability?checkIn=YYYY-MM-DD&checkOut=YYYY-MM-DD&dogIds=a,b→ 200 {runTypes: [{runTypeId, name, freeRuns, quoteCents, depositCents}]} auth: [owner, attendant, manager]POST /api/staysbody: {checkInDate, checkOutDate, runTypeId, dogs: [{dogId, feedingInstructions, ownFood, medications: [{drugName, dose, scheduleTimes, withFood, instructions?}]}], ownerNotes?} → 201 {stayId, status: "pending_deposit", holdExpiresAt, checkoutUrl} | 409 {error: {code: "NO_RUN_AVAILABLE"}} auth: [owner, manager (on behalf,householdIdin body)]GET /api/stays/:stayId→ 200 {stay, dogs, run, payments, balanceCents, vaccineIssues[]} auth: [owner of stay, attendant, manager]PATCH /api/stays/:stayIdbody: {version, checkInDate?, checkOutDate?, dogs?, ownerNotes?} → 200 {stayId, totalCents, version} | 409 {code: "VERSION_CONFLICT"|"NO_RUN_AVAILABLE"} auth: [owner of stay before check-in (care fields only), manager]POST /api/stays/:stayId/run-assignmentbody: {runId} → 200 {runId} | 409 auth: [manager]POST /api/stays/:stayId/cancelbody: {reason, refundOverrideCents?} → 200 {status: "cancelled", refundCents} auth: [owner of stay, manager; override manager only]POST /api/stays/:stayId/check-in→ 200 {status: "checked_in", checkedInAt} | 409 {code: "VACCINES_NOT_VERIFIED", missing[]} auth: [attendant, manager]POST /api/stays/:stayId/check-out→ 200 {status: "checked_out", balanceCents} auth: [attendant, manager]POST /api/stays/:stayId/no-show→ 200 {status: "no_show"} auth: [manager]
Payments
POST /api/stays/:stayId/balance-checkout→ 200 {checkoutUrl} auth: [owner of stay, attendant, manager] - also emailed to the owner at checkoutPOST /api/stays/:stayId/paymentsbody: {method: "in_person_card"|"cash", amountCents} → 201 {paymentId, balanceCents} auth: [attendant, manager]POST /api/webhooks/stripebody: raw Stripe event → 200 {received: true} auth: [public, Stripe-Signature] - handlescheckout.session.completed,checkout.session.expired,charge.refunded
Care floor
GET /api/board?date=YYYY-MM-DD→ 200 {arrivals[], departures[], inHouse: [{stayDogId, dog, run, feedingInstructions, dosesDue: [{stayMedicationId, scheduledFor, logged}]}]} auth: [attendant, manager]POST /api/stay-dogs/:stayDogId/care-logsbody: {kind, stayMedicationId?, scheduledFor?, outcome, note?} → 201 {careLogId} | 409 {code: "DOSE_ALREADY_LOGGED", loggedBy, loggedAt} auth: [attendant, manager]POST /api/stay-dogs/:stayDogId/incidentsbody: {severity, description, actionTaken, occurredAt} → 201 {incidentId, ownerNotifiedAt} auth: [attendant, manager]GET /api/stays/:stayId/incidents→ 200 {incidents[]} auth: [owner of stay, attendant, manager]
Daily updates
POST /api/stays/:stayId/daily-updatesbody: {forDate, message, photoKeys[]} → 201 {dailyUpdateId} | 409 (one per stay per day; use PATCH) auth: [attendant, manager]PATCH /api/daily-updates/:dailyUpdateIdbody: {message?, addPhotoKeys?, removePhotoIds?} → 200 {dailyUpdateId} auth: [attendant, manager]GET /api/stays/:stayId/daily-updates→ 200 {updates: [{forDate, message, photos: [{photoId, status}]}]} auth: [owner of stay, attendant, manager]GET /api/photos/:photoId?size=thumb|display→ 302 to a 5-minute presigned GET URL auth: [owner of the photo's stay, attendant, manager]
Configuration and audit
GET /api/settings→ 200 {KennelSettings} auth: [attendant, manager]PUT /api/settingsbody: {depositPercent, holdMinutes, cancellationWindowHours, checkInStart, checkOutEnd, requiredVaccines, photoRetentionDays, timeZone, kennelName} → 200 {KennelSettings} auth: [manager]POST /api/run-typesbody: {name, nightlyRateCents, extraDogRateCents, maxDogs} → 201 {runTypeId} auth: [manager];PATCH /api/run-types/:runTypeId→ 200 auth: [manager]POST /api/runsbody: {code, runTypeId, notes?} → 201 {runId} auth: [manager];PATCH /api/runs/:runIdbody: {active?, runTypeId?, notes?} → 200 auth: [manager]GET /api/audit-events?subjectType&subjectId&cursor→ 200 {events[], nextCursor} auth: [manager]
Interface: owner pages (owner)/dogs, (owner)/stays/[stayId] (status, payments, updates, incidents); staff pages (staff)/board, (staff)/desk (arrivals/departures), (staff)/vaccinations (review queue), (staff)/settings. Every email link points to an owner portal page; an owner without a session is sent a magic link to the same email, so no password is needed.
6. KEY DEPENDENCIES AND RATIONALE
Versions are approximate; verify exact versions against the npm registry at implementation time.
| Library | Current stable major version | Must pair with | Purpose in this project |
|---|---|---|---|
| Node.js | 24 LTS | - | Runtime for web and worker; active LTS through the planned life of v1 (verify Prisma engine support). |
| next | 16 | react 19, Node ≥ 20.9 | App Router pages and route handlers; one deployable for both portals instead of a separate SPA and API. |
| react / react-dom | 19 | next 16 | UI; the major Next.js 16 requires. |
| typescript | 5 | - | Types shared by route handlers, services and components. |
| prisma / @prisma/client | 6 | Node 24 (verify), PostgreSQL 17 | Schema, migrations and typed queries; kept on 6 because its migrate workflow and Unsupported columns are the documented path for the hand-written exclusion constraint (7 changes generator config; reassess after v1). |
| PostgreSQL | 17 | btree_gist extension | Single source of truth incl. job queue; exclusion constraints prevent run double-booking. |
| better-auth | 1 | @prisma/client 6, next 16 | Magic-link login for owners and password + TOTP for staff in one library with a Prisma adapter, chosen over Auth.js for built-in two-factor and credential support. |
| stripe (stripe-node) | 19 (verify) | Node 24 | Checkout sessions, refunds, webhook signature verification; Checkout keeps card data off our servers. |
| pg-boss | 11 (verify) | PostgreSQL 13+, Node 20+ | Durable jobs and cron in the existing database: hold expiry, photo processing, emails, retention; avoids running Redis for ~10 req/sec. |
| @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner | 3 | Node 24 | Presigned PUT/GET against Cloudflare R2's S3 API; storage-vendor neutral. |
| sharp | 0.34 | Node 24 (prebuilt linux-x64 binary) | EXIF stripping, rotation and thumbnails in the worker. |
| postmark | 4 | Node 24 | Transactional email (magic links, confirmations, incidents, updates); chosen over SES for deliverability without domain warm-up at low volume. |
| zod | 4 | typescript 5 | Request body and environment validation. |
| date-fns + @date-fns/tz | 4 / 1 | - | Night counts and "today" in the kennel time zone without a heavy date library. |
| pino | 9 | Node 24 | Structured JSON logs to Fly's log stream. |
| @sentry/nextjs | 10 (verify) | next 16 | Error tracking for web and worker. |
| tailwindcss | 4 | next 16 (PostCSS plugin) | Styling for phone-first owner pages and tablet staff board. |
| vitest | 3 | Node 24, vite 6/7 | Unit and integration tests; Next.js documents Vitest setup. |
| @playwright/test | 1 | Node 24 | End-to-end tests of booking and check-in flows. |
| eslint + eslint-config-next | 9 / 16 | next 16 | Lint, incl. no-restricted-imports banning @prisma/client outside src/server/repos/ and src/server/db.ts. |
7. ENVIRONMENT AND CONFIG STRATEGY
| Concern | Variable | Required | Notes |
|---|---|---|---|
| Database | DATABASE_URL | yes | Direct (non-pooled) Postgres URL for web, worker and migrations. |
| App | APP_URL | yes | Public origin, used for email links and Stripe return URLs; also Better Auth base URL. |
| Auth | BETTER_AUTH_SECRET | yes | Session signing/encryption secret, ≥ 32 random bytes. |
| Payments | STRIPE_SECRET_KEY | yes | sk_test_… outside production. |
| Payments | STRIPE_WEBHOOK_SECRET | yes | whsec_… for /api/webhooks/stripe. |
| Storage | S3_ENDPOINT | yes | https://<account>.r2.cloudflarestorage.com. |
| Storage | S3_BUCKET | yes | Private bucket for vaccine documents and photos. |
| Storage | S3_ACCESS_KEY_ID | yes | R2 API token scoped to the bucket. |
| Storage | S3_SECRET_ACCESS_KEY | yes | Paired secret. |
POSTMARK_SERVER_TOKEN | yes | Transactional stream token. | |
EMAIL_FROM | yes | Verified sender, e.g. stays@kennel.example. | |
| Observability | SENTRY_DSN | no | Empty disables Sentry. |
| Observability | LOG_LEVEL | no | info default. |
# .env.example
DATABASE_URL=postgresql://kennel:kennel@localhost:5432/kenneldesk
APP_URL=http://localhost:3000
BETTER_AUTH_SECRET=replace-with-32+-random-bytes
STRIPE_SECRET_KEY=sk_test_replace
STRIPE_WEBHOOK_SECRET=whsec_replace
S3_ENDPOINT=http://localhost:9000
S3_BUCKET=kenneldesk-dev
S3_ACCESS_KEY_ID=minio
S3_SECRET_ACCESS_KEY=minio-secret
POSTMARK_SERVER_TOKEN=POSTMARK_API_TEST
EMAIL_FROM=stays@kennel.example
SENTRY_DSN=
LOG_LEVEL=debug
Loading strategy: Next.js loads .env* files locally; in production Fly injects secrets as environment variables (fly secrets set). src/env.ts parses process.env with a zod schema on first import and throws on any missing or malformed value, so a bad deploy fails the release health check instead of failing mid-request; both next start and worker.ts import it at startup. No variable is NEXT_PUBLIC_: Stripe Checkout is a redirect, so the browser needs no key. Business settings (deposit %, time zone, required vaccines, hold minutes) are not environment variables - they live in the KennelSettings row and are edited at PUT /api/settings. Local storage uses MinIO from docker-compose.yml; POSTMARK_API_TEST makes Postmark accept and discard mail.
Connects securely to your tools. The creator never sees your data.
About this skill
Stop guessing at system architecture and building on shaky foundations. This skill implements a four-stage pipeline to transform vague app ideas into build-ready technical specifications and verified skeletons. It moves from an interactive intake process to an adversarial review, followed by automated scaffolding and risk-mitigating spikes.
What it does
- Architect blueprints via a structured 12-section technical specification process.
- Conduct adversarial reviews using stack-specific fact sheets for 25 different technologies to catch build-blockers early.
- Generate walking skeletons by scaffolding basic project structures, installing packages, and verifying builds.
- Execute technical spikes to prove out risky integrations or performance assumptions before full development begins.
- Automate version control for blueprints, ensuring every repair or decision is tracked in sequential versions.
How it works
- Intake: Provide an app idea or use the idea picker script to generate five core inputs for the initial blueprint.
- Review: Run the blueprint through a subagent that compares the plan against specific technology fact sheets to identify MAJOR or MINOR risks.
- Scaffold: Trigger the creation of a physical project skeleton in a clean directory, including a
verify.shscript to confirm the environment is viable. - Spike: Approve time-boxed experiments to test high-risk features, with the skill automatically updating the blueprint based on the results.
Frameworks & tools
This tool relies on Python 3 scripts for prompt rendering and section merging. It uses specialized fact sheets for 25 stacks including TypeScript, Python, and Go, and interfaces with Bash for filesystem operations and environment verification.
Why this beats prompting it yourself
Generic prompts often miss subtle configuration conflicts or security anti-patterns specific to a tech stack. This skill uses an adversarial multi-agent workflow and 25 hardware/software fact sheets to catch errors that standard LLM context windows typically ignore.
Use cases
- Drafting a comprehensive tech spec for a new SaaS product from a single paragraph.
- Validating an existing architecture plan against specific Python or Next.js best practices.
- Prototyping a "walking skeleton" with working database gates and CI/CD stubs.
- Testing a risky third-party API integration via a time-boxed spike.
Known limitations
The skill requires a local Python 3.9 environment for its helper scripts. Scaffolding and spikes require an empty directory and may require Docker for specific database gates.
How to use
Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill passes our trust checks below.
Security scanned
Passed our 8-point scan before listing
Fresh listing
Recently published to Agensi
Money-back guarantee
Doesn't do what its page promises? You get your money back.
Trust & safety
Security scanned
Verified clean today
- Passed all security checks, Safe to install
Requires