Imported from kent/listygifty (
AGENTS.md). Install upstream withnpx skills add kent/listygifty. Copyright stays with the author.
Listy Gifty Development Guidelines
Overview
Listy Gifty is a gift planning application built as a monorepo with:
- Rails API backend
- Next.js web frontend
- React Native/Expo mobile app (Listy Gifty)
- Shared packages for types, API client, and services
Architecture
Monorepo Structure
niftygifty/
├── apps/
│ ├── api/ # Rails 8 API-only backend
│ ├── web/ # Next.js 16 frontend (Listy Gifty)
│ └── mobile/ # Expo/React Native app (Listy Gifty) ⚠️ SPECIAL
├── packages/
│ ├── types/ # Shared TypeScript types
│ ├── api-client/ # Shared API client
│ └── services/ # Shared service layer
├── package.json # Root workspace config
└── turbo.json # Turborepo config
Technology Stack
- Backend: Rails 8.1, PostgreSQL (Cloud SQL for staging/prod; SQLite optional in local/dev), Devise + Clerk, Blueprinter
- Web Frontend: Next.js 16, React 19, Tailwind CSS, shadcn/ui
- Mobile App: Expo SDK 54, React Native 0.81, React 19.1
- Shared: TypeScript packages for types, API client, services
- Auth: Clerk (web + mobile)
⚠️ IMPORTANT: Mobile App Build Process
The mobile app (apps/mobile) is intentionally DECOUPLED from the monorepo npm workspaces.
Why?
- React Native requires React 19.1.0
- Web app uses React 19.2.0
- npm workspace hoisting causes version conflicts
How It Works
The mobile app:
- Is excluded from root
package.jsonworkspaces (onlyapps/webis included) - Uses
file:references to link shared packages:"@niftygifty/types": "file:../../packages/types", "@niftygifty/api-client": "file:../../packages/api-client", "@niftygifty/services": "file:../../packages/services" - Has its own
node_modulesandpackage-lock.json - Should be built and tested independently
Mobile App Commands
# Always run FROM the mobile directory
cd apps/mobile
# Install dependencies (use --legacy-peer-deps for Clerk compatibility)
npm install --legacy-peer-deps
# Start development
npx expo start
# Run tests
npm test
# Build for iOS/Android
npx expo build:ios
npx expo build:android
TestFlight Default
When the user asks for a Listy Gifty TestFlight release, default to the App Store Connect internal testing group Internal Testers for kent.fenwick@gmail.com.
Rules:
- Do not default production TestFlight releases to
Staging. - Prefer the production iOS EAS submit path for TestFlight unless the user explicitly asks for a different profile.
- Ensure production submits target the
Internal Testersgroup unless the user explicitly asks for a different tester group.
Cursor Mobile Releases
- Treat natural-language requests such as
deploy to TestFlight,ship to TestFlight,release to TestFlight,test flight, and obvious misspellings such asdeploy to test lfihgtas explicit authorization to create a new production TestFlight build. Do not require the user to mention GitHub Actions,/testflight, EAS, credentials, or a release profile. - A request to configure, document, or verify this release automation is not itself authorization to queue a build. Only act when the user clearly asks to deploy, ship, or release the app to TestFlight.
- For an actual release request, finish the normal pull request flow, wait for
all required checks, merge without bypassing branch protection, and use the
exact merge commit on
main. If the change affects the API, web, mobile, shared packages, infrastructure, or deploy scripts, wait for the productionDeployrun for that exact commit to succeed before starting TestFlight. - Queue TestFlight by commenting exactly
/testflighton the merged pull request. If the currentmaincommit has no suitable merged pull request, dispatchMobile Releasemanually frommainwithrelease_actionset totestflightandeas_profileset toproduction. - Monitor the
Mobile Releaseworkflow through EAS build, Apple processing, and tester-group verification. Report the build number and workflow/build links; merely queueing the workflow is not completion. - Cursor Cloud agents do not need Apple or Expo release credentials. Keep
EXPO_TOKENandAPP_STORE_CONNECT_API_KEY_P8only in GitHub Actions. - Production backend/web deployment starts automatically for relevant
mainchanges. The phone-accessible recovery path is a manualmainrun of theDeployworkflow; enable its mobile input to queue TestFlight only after the backend rollout and smoke tests succeed. - TestFlight builds must come from commits already merged into
main. - See
docs/mobile-release.mdfor the complete phone workflow and recovery steps.
Before Building Mobile
Ensure shared packages are built first:
# From repo root
npm run build # Builds types, api-client, services, web
Building & Testing
Quick Reference
| App/Package | Build Command | Test Command |
|---|---|---|
| All (except mobile) | npm run build |
npm run test |
| API | N/A (Ruby) | cd apps/api && bin/rails test |
| Web | cd apps/web && npm run build |
cd apps/web && npm run lint |
| Mobile | cd apps/mobile && npx expo start |
cd apps/mobile && npm test |
| Types | cd packages/types && npm run build |
N/A |
| API Client | cd packages/api-client && npm run build |
N/A |
| Services | cd packages/services && npm run build |
N/A |
Full Build (CI-style)
# 1. Install root dependencies
npm install
# 2. Build all packages and web
npm run build
# 3. Run API tests
cd apps/api && bin/rails test
# 4. Build and test mobile (separate process)
cd apps/mobile
npm install --legacy-peer-deps
npm test
Lint All
./bin/lint # Runs RuboCop + Next.js lint/typecheck
Core Principles
1. Don't Repeat Yourself (DRY)
Types: All shared types live in packages/types/src/index.ts. Before creating a new type:
- Search the types package first
- If it exists, import it:
import type { Holiday } from "@niftygifty/types" - Only create new types if they don't exist
Services: Shared services live in packages/services/:
holidays.service.tsgifts.service.tsgift-statuses.service.ts
API Client: Shared API client in packages/api-client/
1.1 Clean Code Rules (Required)
- Extract repeated logic before it appears in a third place.
- Keep API access in service layers (
packages/services,@/lib/api) and out of UI components. - Keep components focused on UI and screen orchestration, not request formatting/parsing.
- Prefer existing shared utilities/components over copy/paste:
- Mobile formatters:
apps/mobile/lib/formatters.ts - Mobile status mapping:
apps/mobile/lib/gift-status-colors.ts - Mobile reusable states:
apps/mobile/components/ScreenLoader.tsx,apps/mobile/components/InlineError.tsx,apps/mobile/components/FloatingActionButton.tsx
- Mobile formatters:
- For behavior changes, add or update tests in the affected app.
2. Service Layer Pattern
Components should NEVER make direct API calls. Always use services:
// Web app
import { holidaysService } from "@/services";
// Mobile app
import { holidaysService } from "@/lib/api";
// Usage is identical
const holidays = await holidaysService.getAll();
3. Type Synchronization
Rails Blueprints and TypeScript types must stay in sync:
Rails Blueprint (apps/api/app/blueprints/holiday_blueprint.rb):
class HolidayBlueprint < ApplicationBlueprint
fields :name, :date, :created_at, :updated_at
end
TypeScript Type (packages/types/src/index.ts):
export interface Holiday extends BaseEntity {
name: string;
date: string;
created_at: string;
updated_at: string;
}
4. Authentication Flow
Both web and mobile use Clerk for authentication:
Web: Uses @clerk/nextjs with middleware
Mobile: Uses @clerk/clerk-expo with SecureStore for token caching
The Rails API validates Clerk JWTs and syncs users on first request.
5. Error Handling
Services throw ApiError on failure:
import { ApiError } from "@niftygifty/api-client";
try {
await holidaysService.getAll();
} catch (err) {
if (err instanceof ApiError) {
if (err.isUnauthorized) {
// Handle 401
}
console.error(err.message);
}
}
Development Workflow
Starting Development
# Terminal 1: API
cd apps/api && bin/rails server -p 3001
# Terminal 2: Web
cd apps/web && npm run dev
# Terminal 3: Mobile (optional)
cd apps/mobile && npx expo start
Adding a New Feature
- Define types in
packages/types/src/index.ts - Rebuild packages:
npm run build(from root) - Create/update blueprint in
apps/api/app/blueprints/ - Create/update controller in
apps/api/app/controllers/ - Add service method in
packages/services/if shared - Build UI in web and/or mobile using shared services/utilities (no direct API calls in components)
- Run tests in all touched apps
- Deploy to staging first, then production after validation
Adding a New API Endpoint
- Add route to
apps/api/config/routes.rb - Create controller action
- Create/update blueprint for serialization
- Add TypeScript types to
packages/types - Add service method to
packages/services - Rebuild:
npm run build
File Naming Conventions
- Services:
*.service.ts(e.g.,holidays.service.ts) - Types: PascalCase interfaces (e.g.,
Holiday,CreateHolidayRequest) - Components: PascalCase (e.g.,
HolidayCard.tsx) - Pages: lowercase with hyphens
API Endpoints
| Resource | Endpoint | Methods |
|---|---|---|
| Auth | /users, /users/sign_in, /users/sign_out |
POST, DELETE |
| Holidays | /holidays |
GET, POST, PATCH, DELETE |
| People | /people |
GET, POST, PATCH, DELETE |
| Gifts | /gifts |
GET, POST, PATCH, DELETE |
| Gift Statuses | /gift_statuses |
GET; global mutations require an allowlisted Clerk admin (admin MCP is preferred) |
| Gift Exchanges | /gift_exchanges |
GET, POST, PATCH, DELETE |
Environment Variables
Web (apps/web/.env.local)
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
Mobile (apps/mobile/.env)
EXPO_PUBLIC_API_URL=http://localhost:3001
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
API
Uses Rails credentials. Clerk keys configured in credentials or environment.
GCP Deployment Profile (listygifty)
Use this profile for all Cloud Run/Cloud Build deploy actions so commands never target the wrong project.
- Project ID:
listygifty - Project number:
906707282968 - Region:
us-central1 - Deployer service account:
niftygifty-deployer@listygifty.iam.gserviceaccount.com - GitHub Actions SA:
github-actions@listygifty.iam.gserviceaccount.com - Local key file (gitignored):
.gcp/keys/listygifty-deployer.json - Local profile file (gitignored):
.gcp/listygifty-deploy.env
gcloud Configuration Profile
A named gcloud configuration listygifty is set up to avoid account switching issues:
# Activate the listygifty profile before running any gcloud commands
gcloud config configurations activate listygifty
This profile is pre-configured with:
- Account:
kent.fenwick@gmail.com - Project:
listygifty
Activate deploy profile
gcloud config configurations activate listygifty
source .gcp/listygifty-deploy.env
Deploy commands
Everything ships through Pulumi (infra/pulumi/). Production only — staging
is turned off pre-PMF (see infra/pulumi/README.md to re-enable):
source .gcp/listygifty-deploy.env
npm run deploy # web + API
npm run deploy:mobile # web + API + iOS EAS build
Each command runs (parallel where independent): API + web image builds via
Cloud Build, db:migrate via Cloud Run job, Cloud Run rollout, smoke tests
(/up, /holidays → 401, web /, /login). The EAS mobile build
(--no-wait --auto-submit) only runs with ENABLE_MOBILE=true /
deploy:mobile. Repeating a git SHA still runs both Cloud Builds; Pulumi then treats unchanged rollout resources and command triggers as idempotent.
Pulumi state lives in gs://listygifty-pulumi-state (self-hosted GCS
backend). See infra/pulumi/README.md for bootstrap, import-existing,
rollback, and expected timings.
Branch deploy policy
- Push to
staging-> deploy staging (via GitHub Actions wrappingpulumi up) - Push to
main-> deploy production (same) - Both flows ultimately invoke the same Pulumi program; the bash/GHA wrapper exists only to compute the source SHA and gate by branch.
If the deployer key is missing
mkdir -p .gcp/keys
CLOUDSDK_CORE_ACCOUNT=kent.fenwick@gmail.com CLOUDSDK_CORE_PROJECT=listygifty \
gcloud iam service-accounts keys create .gcp/keys/listygifty-deployer.json \
--iam-account=niftygifty-deployer@listygifty.iam.gserviceaccount.com
chmod 600 .gcp/keys/listygifty-deployer.json .gcp/listygifty-deploy.env