Skip to content

Contributing

Thanks for your interest. GiftWrapt is a personal project, but contributions are welcome. This page covers the dev workflow once your local stack is up. For first-time setup see Getting started.

src/
routes/ TanStack Router file-based routes
components/ UI components (shadcn-derived in components/ui)
db/ Drizzle schema and queries
lib/ Server-side helpers (auth, storage, scraping, email)
emails/ React Email templates
api/ Server-only utilities and integrations
drizzle/ Generated SQL migrations (committed)
docker/ Self-host compose files and runtime scripts
docs/ All long-form documentation
scripts/ CLI entry points (admin, seed, storage init)
CommandWhat it does
pnpm devRun migrations and start the dev server on :3000.
pnpm buildProduction build (Nitro server + standalone CLI bundles).
pnpm testUnit and Storybook tests via Vitest.
pnpm test:integrationIntegration tests (requires Postgres).
pnpm test:allEverything.
pnpm lintESLint over the whole tree.
pnpm formatPrettier check.
pnpm checkFormat and autofix lint. Run before committing.
pnpm storybookStorybook on :6006.
pnpm dev-emailReact Email preview server on :3001.
CommandWhat it does
pnpm db:generateGenerate a SQL migration from schema changes. Commit the output.
pnpm db:migrateApply pending migrations.
pnpm db:pushPush the schema directly (dev only, skips migrations).
pnpm db:studioDrizzle Studio.
pnpm db:seedSeed local DB with test users and data. Requires SEED_SAFE=1. Truncates everything.

The local seeded admin and other test users are documented in local-dev-admin.md.

Component stories live next to their components (*.stories.tsx). Storybook runs as a Vitest project so stories are typechecked and smoke-tested on every pnpm test. New UI work should ship with a story for the interesting states.

  • React 19, React Compiler is enabled. Don’t reach for useMemo or useCallback reflexively; profile first.
  • Server functions and loaders live next to their routes. Pure data helpers go in src/lib/ or src/db/queries/.
  • Prefer Drizzle’s relational query API over hand-rolled SQL where it fits.

Components in src/components/ui/ are owned by this repo, not pulled from a package. See shadcn-upgrades.md for how to pull upstream improvements without losing local customizations.

Tailwind CSS v4. Follow the existing cn() + class-variance-authority patterns in src/components/ui/.

Conventional Commits, imperative mood, ≤72 chars on the subject:

feat(lists): add bulk archive action
fix(scraping): fall back to og:image when product image is missing
docs(self-host): document RustFS bootstrap

Pre-commit hooks run lint-staged (Prettier + ESLint on staged files). Commitlint enforces the format.

release-please watches main and opens a PR with the next version bump and CHANGELOG.md entry derived from commit messages. Merging that PR tags a release and triggers the GHCR image publish (ghcr.io/shawnphoffman/giftwrapt:vX.Y.Z plus :latest).

State-changing TanStack server functions and route handlers should always use method: 'POST'. The CSRF posture for those calls relies entirely on the auth cookie’s SameSite=Lax attribute (better-auth default, set in src/lib/auth.ts):

  • Lax prevents the auth cookie from being attached to cross-origin POST requests. An attacker page on evil.example can render a form that posts to our server function, but the browser strips the cookie, the auth middleware sees no session, and the call is refused.
  • Top-level navigation GETs do carry the Lax cookie, but state changes are POST-only, so a malicious link can’t trigger one. Reads (method: 'GET' server fns) are CSRF-irrelevant by definition; they don’t change state.
  • Same-site XSS would defeat this (along with any explicit CSRF token), so the Content-Security-Policy headers in vite.config.ts and the existing input-validation pattern (every server fn uses .inputValidator() with zod) are the actual XSS defense.

Concretely, when adding a new server function or route:

  • Use createServerFn({ method: 'POST' }) for anything that writes. Never accept writes via GET.
  • Don’t enable crossSubDomainCookies on better-auth without re-evaluating this posture; sharing the cookie across subdomains widens the trust boundary.
  • Don’t set your own cookies bypassing better-auth’s cookie helpers. If you need one, ensure httpOnly: true, sameSite: 'lax' (or 'strict'), secure on HTTPS.

See sec-review L6.

  1. Branch from main.
  2. Run pnpm check and pnpm test before opening the PR.
  3. If you touched the schema, commit the generated migration in drizzle/.
  4. If you touched UI, add or update a Storybook story.
  5. Note any new env vars in env.example and the relevant doc.