Skip to content

Latest commit

 

History

History
731 lines (649 loc) · 56.1 KB

File metadata and controls

731 lines (649 loc) · 56.1 KB

3D Models Gallery

A modern web application for browsing and discovering 3D models, built with Next.js, TypeScript, and Drizzle ORM.

🛠️ Tech Stack

Next.js React TypeScript Panda CSS Drizzle ORM Better Auth next-safe-action Biome Ultracite Formatted with Biome Linted with Biome

  • Framework: Next.js 16.4.0-canary.34 with App Router, Cache Components, React Compiler, typed routes (typedRoutes), experimental useOffline, and root maxDuration = 45 (platform hard kill ceiling)
  • Language: TypeScript 7.0.2 with React 19.3 (19.3.0); bunfig minimumReleaseAge (3 days) excludes Next/SWC plus Panda compiler/preset packages
  • Styling: Panda CSS 2.0.0-beta.17 (@pandacss/dev, @pandacss/preset-base, @pandacss/preset-panda, @pandacss/preset-typography); thin panda.config.ts plus theme/ define* blocks (defineKeyframes, defineTokens, defineSemanticTokens, defineConditions, definePattern, defineUtility, defineGlobalStyles); generated styled-system/ from panda build (gitignored; run via bun install / prepare); imports use the @styled-system/* path alias (tsconfig.json); @layer stack in src/app/index.css; shared theme keyframes in theme/keyframes.ts; scroll-driven / view-timeline animations stay local via @styled-system/css keyframes() (navbar, scroll-progress, categories mask, model-card); optimize.removeUnusedKeyframes: false; shared Skeleton uses shimmer CSS vars (color / highlightColor props); @pandacss/mcp 2.0.0-beta.17 for agent token lookup
  • Database: Turso (libSQL / SQLite) with Drizzle ORM 1.0.0-rc.4 (dialect: "turso", @libsql/client); model PK is uuidv7 ModelId (uuidv7 + src/db/create-id.ts)
  • Authentication: Better Auth 1.7.4 with email/password and GitHub OAuth, cookie caching enabled, ElysiaJS API backend; Drizzle adapter uses relations-v2 with advanced.database.joins (provider: "sqlite"); account.issuer dropped (src/db/migrations/20260911114623_better_auth_1_7_3_drop_issuer/)
  • Server Actions: next-safe-action 8.7.3 (lib/safe-action.ts actionClient / authActionClient, flattened Valibot validationErrors); formDataInput maps <form> FormData onto object schemas; client useStateAction / useOptimisticStateAction; lib/safe-action-form.ts lastNonSecretFormValue for non-password defaultValue
  • Search Params: nuqs 2.10.1 for type-safe URL state (query, cursor, direction, limit, sort); NuqsAdapterBoundary wraps the 3d-models layout so search works on index and category routes; listing canonical URLs use nuqs/server loaders/serializers (features/models/listing/listing-canonical.ts) with clearOnDefault for SEO metadata (omits default cursor/direction/limit/sort and empty query; drops direction when cursor is absent); model detail from return paths are allowlisted via features/models/listing/listing-path.ts; component tests wrap withNuqsTestingAdapter via tests/setup/nuqs-testing.ts (withListingNuqsTestingAdapter)
  • Linting & Formatting: Biome 2.5.12 with Ultracite 7.11.1 presets (ultracite/biome/core, react, next); Fallow 3.25.0 with boundaries.preset: "bulletproof" (.fallowrc.json); health maxCyclomatic / maxCognitive 12, maxCrap 30 (src/db/** override 80); unused-dependencies, prop-drilling, thin-wrapper, duplicate-prop-shape at error; React Doctor 0.9.14 on PRs and pushes to main (.github/workflows/react-doctor.yml, SHA-pinned actions, doctor.config.ts) and on staged TS/TSX via Husky + lint-staged (type, react-doctor:staged); Cursor agent hooks in .cursor/hooks.json (afterFileEdit: Ultracite fix skipping unused-import removal + test:affected; stop: full fix, fallow audit, full test); Panda MCP in .cursor/mcp.json / AGENTS.md
  • Type Checking: TypeScript 7 via tsc (bun run type / typegen); Next build uses project-local tsc (experimental.useTypeScriptCli in next.config.ts) because TS 7 has no JS compiler API
  • Package Manager: Bun (install, tests, Drizzle scripts, prepare); bunfig.toml sets minimumReleaseAge (3 days) with Next/SWC/Panda excludes
  • Next.js runtime: Bun is the desired runtime. Local dev, production build, start, and next:analyze still run Next on Node (varlock run -- bun run next dev, bun varlock run -- next build, bun run next start, bun varlock run -- next experimental-analyze). dev:inspect, build:debug, and next:upgrade use bun --bun. Cache Components + partialPrefetching on Bun leak Next prerender abort rejects as CLIENT_HOOK_DYNAMIC (usePathname / useSearchParams “outside <Suspense>”) during bun --bun next build: exit 0, routes stay PPR, Node next build silent on the same tree. export const instant = false does not silence this — it only opts that segment out of Instant Navigation validation and does not disable prerender. Runtime bun server.js can also leak AbortError / Date.now() unhandled rejections because Bun drops AsyncLocalStorage on unhandledRejection. Track oven-sh/bun#39847, oven-sh/bun#40223, vercel/next.js#97663. The older setTimeout / _idleStart warning (vercel/next.js#87630, oven-sh/bun#26508) is fixed in Bun 1.4.0 and is not the current blocker. Re-enable bun --bun for dev / build / start when the ALS / hanging-promise fixes land.
  • Build Tool: Turbopack for dev and build; partialPrefetching, MCP server, cached navigations, Turbopack filesystem caches (turbopackFileSystemCacheForDev / ForBuild), and turbopackRustReactCompiler (next.config.ts); env types from Varlock (.env.schema, src/env.d.ts), not Next typedEnv
  • Environment: Varlock 1.19.0 with .env.schema (@currentEnv=$VARLOCK_ENV, @encryptInjectedEnv=forEnv(production, preview)), committed .env.test fixtures for Bun tests (no Bitwarden fetch), @varlock/nextjs-integration plugin in next.config.ts, optional Bitwarden Secrets Manager via @varlock/bitwarden-plugin (see docs/VARLOCK.md)
  • Validation: Varlock for environment; Valibot 1.5.0 for next-safe-action input schemas; model slugs validated via slugify idempotency (lib/slugify.ts + isModelSlug in db/brands.ts)

🚀 Features

  • Browse 3D Models: View a curated collection of 3D models across various categories
  • Category Filtering: Filter models by category (3D Printer, Art, Education, Fashion, etc.)
  • Sort Controls: Sort listings by A-Z, Popular, or Recent via nuqs sort search param (default A-Z omitted from URL); ties break on ModelId
  • Cursor pagination: Exclusive keyset on (sortCol, ModelId) with URL cursor (uuidv7 ModelId) + direction (forward/backward) + limit; next/prev flags from limit+1 (no COUNT(*)); old ?page= bookmarks land on the first slice. sort=popular is a live feed: likes can skip/dup between slices (accepted). ADR: docs/adr/0001-cursor-pagination.md. Domain terms: CONTEXT.md. Survey: docs/MUTABLE_SORT_KEYSET.md
  • Search across listings: Search bar lives in ModelsGridHeader (index + category routes); grid title shows Results for "{query}" via nuqs when a query is present; search/sort/limit reset cursor rather than paging
  • Model detail back link: Detail pages restore the prior listing via allowlisted from query (features/models/back-link/) with runtime prefetch under Partial Prefetching
  • Shimmer skeletons: Shared Skeleton shimmer (CSS vars / color props) for listing and detail loading states
  • Offline indicator: Navbar OfflineBanner via Next.js experimental useOffline — persistent pill with a wifi-off icon and container-responsive status detail; visible on all breakpoints (components/offline-indicator.tsx)
  • Mobile navigation: Below sm (640px), navbar uses native <button popovertarget> + popover="auto" menu; the compact icon logo is used below xs (480px), with the full wordmark from xs upward (src/components/navbar/)
  • Categories sidebar: From md, sticky nav with header clearance (insetBlockStart: 6.5rem) and overflowY: auto (maxBlockSize: calc(100dvh - 7.5rem)); small-screen horizontal strip uses a local scroll-driven fade mask (keyframes() in app/3d-models/layout.tsx)
  • Responsive Design: Optimized for desktop, tablet, and mobile devices
  • Smooth Page Transitions: View Transitions API; pager uses addTransitionType forwards/backwards on PaginationPageSlice (full-viewport slide on small screens, compact from md). Search/sort/limit are not page turns
  • Type-Safe Database: Full TypeScript support with Drizzle ORM
  • Performance Optimized: Caching for frequently accessed data; listing DAL combines cache + timeout abort signals (utils/with-abort.ts, ABORT_TIMEOUT_MS)
  • Modern Stack: Built with Next.js 16.4, TypeScript, and Panda CSS
  • Feature-Based Architecture: Well-organized codebase with clear separation of concerns

Note: Like/dislike functionality with optimistic updates and real-time like count synchronization is fully implemented.

📁 Project Structure

Static assets are served from public/ at the repository root (not under src/), including logos, hero images, and public/img/models/*.avif thumbnails referenced by seed data. Supplemental docs live in docs/ (for example AUTH_SETUP.md, VARLOCK.md, MODEL_CACHE_SPLIT.md, PSEUDO_CLASS_TRANSITIONS.md, PERFORMANCE_IMPROVEMENTS.md, REACT_STINKY.md, docs/adr/0001-cursor-pagination.md). Listing domain language (ModelId, ListingCursor, PageSlice) lives in root CONTEXT.md. Panda CSS config is a thin panda.config.ts that imports theme/ define* modules; generated files go to styled-system/ at the repo root (outdir); that folder is gitignored—run bun install (or bunx panda build) so imports like @styled-system/css resolve. Root tooling includes doctor.config.ts and .github/workflows/react-doctor.yml for PR diagnostics.

theme/                        # Panda define* blocks (imported by panda.config.ts)
├── conditions.ts             # defineConditions (scroll/linear/hover support, data-*)
├── global-styles.ts          # defineGlobalStyles (fonts, scrollbar)
├── keyframes.ts              # Shared fade/slide/shimmer theme animations
├── patterns.ts               # between, hoverShadow
├── semantic-tokens.ts
├── tokens.ts                 # colors, easings, sizes, shadows
└── utilities.ts              # cornerShape
src/
├── app/                          # Next.js App Router
│   ├── @navbar/                  # Parallel route for navbar
│   │   ├── default.tsx
│   │   └── error.tsx
│   ├── @footer/                  # Parallel route for footer
│   │   └── default.tsx
│   ├── 3d-models/                # 3D models routes
│   │   ├── @categories/          # Parallel route for categories nav
│   │   │   ├── default.tsx
│   │   │   └── error.tsx         # Error boundary for categories
│   │   ├── @results/             # Parallel route for search results
│   │   │   ├── [...catchAll]/
│   │   │   │   └── page.tsx
│   │   │   ├── default.tsx
│   │   │   ├── error.tsx         # Error boundary for results with retry functionality
│   │   │   ├── loading.tsx       # Loading state for results
│   │   │   └── page.tsx
│   │   ├── [slug]/               # Individual model page
│   │   │   ├── error.tsx         # Error boundary for model detail page
│   │   │   ├── not-found.tsx
│   │   │   └── page.tsx
│   │   ├── categories/           # Category-specific pages
│   │   │   └── [categoryName]/
│   │   │       ├── error.tsx     # Error boundary for category pages with retry functionality
│   │   │       ├── loading.tsx   # Loading state for category pages
│   │   │       ├── not-found.tsx
│   │   │       └── page.tsx
│   │   ├── layout.tsx            # Models layout: sticky categories overflow + local scroll-mask keyframes
│   │   └── page.tsx              # Models landing page
│   ├── about/                    # About page
│   │   └── page.tsx
│   ├── (auth)/                   # Authentication group route
│   │   ├── layout.tsx            # Centered auth layout
│   │   ├── signin/
│   │   │   └── page.tsx
│   │   └── signup/
│   │       └── page.tsx
│   ├── api/                      # API routes
│   │   └── [[...slugs]]/
│   │       ├── better-auth-openapi.ts  # Better Auth OpenAPI spec for Elysia docs
│   │       └── route.ts          # ElysiaJS handler mounting Better Auth (`basePath` /api/auth)
│   ├── index.css                 # Global `@layer` stack (reset → utilities)
│   ├── icon.png                  # App icon (metadata)
│   ├── layout.tsx                # Root layout
│   ├── page.tsx                  # Home page
│   ├── global-error.tsx          # Root error boundary (App Router)
│   ├── robots.ts                 # robots.txt Route Handler
│   └── sitemap.ts                # Sitemap generation
├── instrumentation-client.ts     # Oddbird popover / CSS anchor polyfills
├── features/
│   ├── auth/                     # Auth journeys (sign-in / sign-up / GitHub)
│   │   ├── actions/              # NSA stateActions (Valibot + formDataInput)
│   │   │   ├── sign-in-action.ts
│   │   │   ├── sign-in-github-action.ts
│   │   │   └── sign-up-action.ts  # SignUpData type co-located here
│   │   ├── components/           # Auth page UI
│   │   │   ├── auth-card.tsx
│   │   │   ├── auth-footer-link.tsx
│   │   │   ├── auth-form.tsx      # Shared form (`useStateAction` + lastNonSecretFormValue)
│   │   │   └── sign-in-button.tsx
│   │   └── constants.ts          # Auth validation constants
│   ├── categories/               # Categories feature
│   │   ├── components/
│   │   │   ├── categories-block-transition.tsx
│   │   │   └── categories-nav.tsx
│   │   ├── constants.ts          # ALL_CATEGORIES, CATEGORY_LIST_ITEMS, not-found metadata
│   │   ├── types.ts              # CategoryName (CategorySlug route param)
│   │   └── queries/
│   │       ├── get-all-categories.ts
│   │       ├── get-all-category-slugs.ts
│   │       └── get-category-by-slug.ts
│   ├── models/                   # Models feature
│   │   ├── back-link/            # Detail → listing return path (`from` query)
│   │   │   ├── from-search-params.ts
│   │   │   ├── model-back-link.tsx
│   │   │   └── model-back-link-skeleton.tsx
│   │   ├── components/
│   │   │   ├── model-card.tsx
│   │   │   ├── model-card.styles.ts   # Container-query + subgrid listing card
│   │   │   ├── models-grid.styles.ts  # Listing grid pattern (`modelsGrid`)
│   │   │   ├── model-card-skeleton.tsx
│   │   │   ├── model-detail.tsx
│   │   │   ├── model-detail.styles.ts # Container-query detail layout
│   │   │   ├── models-grid.tsx
│   │   │   ├── models-grid-header.tsx
│   │   │   ├── models-grid-header-skeleton.tsx
│   │   │   ├── models-grid-skeleton.tsx
│   │   │   ├── models-grid-title.tsx
│   │   │   ├── models-grid-title-skeleton.tsx
│   │   │   ├── models-not-found.tsx
│   │   │   ├── models-sort-controls.tsx
│   │   │   ├── models-sort-controls-skeleton.tsx
│   │   │   └── models-view.tsx
│   │   ├── constants.ts
│   │   ├── dal/
│   │   │   ├── get-models.ts     # `{ result, query, isAuthenticated }`; search + user, `withLikeStatuses`
│   │   │   └── search-models.ts  # Unified listing/search + abortable cached awaits
│   │   ├── listing/
│   │   │   ├── listing-canonical.ts  # Canonical path serializer (`query`/`cursor`/`direction`/`limit`/`sort`)
│   │   │   └── listing-path.ts   # Allowlisted listing href → Route (open-redirect safe)
│   │   ├── listing-search-params.ts  # Composes sort + cursor pagination + query nuqs parsers
│   │   ├── sort/                 # Sort sub-feature (nuqs param, order mapping, controls hook)
│   │   │   ├── brands.ts         # Valibot branded Sort type; `isSortList` guard
│   │   │   ├── components/
│   │   │   │   └── sort-option.tsx  # Radio + label pill for one sort value
│   │   │   ├── constants.ts      # SORT_VALUES, DEFAULT_SORT, SORT_LABELS
│   │   │   ├── hooks/
│   │   │   │   └── use-sort-query.ts
│   │   │   ├── order-for-sort.ts # Drizzle orderBy + keyset column/dir (tie-break ModelId)
│   │   │   └── sort-search-params.ts
│   │   ├── likes/                # Likes sub-feature (toggle, status, heart UI)
│   │   │   ├── actions/
│   │   │   │   └── toggle-like.ts
│   │   │   ├── components/
│   │   │   │   ├── heart-button-client.tsx
│   │   │   │   ├── heart-button-count.tsx
│   │   │   │   ├── heart-button-recipe.ts
│   │   │   │   ├── heart-button-server.tsx
│   │   │   │   ├── heart-button-skeleton.tsx
│   │   │   │   ├── heart-sign-in-hint.tsx
│   │   │   │   └── likes-count-transition.tsx
│   │   │   ├── dal/
│   │   │   │   └── toggle-like.ts
│   │   │   ├── hooks/
│   │   │   │   ├── heart-like-optimistic.ts
│   │   │   │   └── use-heart-like.ts
│   │   │   ├── queries/
│   │   │   │   └── like-status.ts
│   │   │   ├── constants.ts
│   │   │   ├── types.ts
│   │   │   └── with-like-status.ts  # Map models → `hasLiked` from liked slug set
│   │   ├── queries/
│   │   │   ├── build-models-where.ts  # Shared SQL where builder for list filter
│   │   │   ├── get-all-model-slugs.ts
│   │   │   ├── get-model-by-slug.ts
│   │   │   └── get-models-list.ts     # Keyset list (`limit+1`); no COUNT(*)
│   │   └── types.ts              # ModelWithLikeStatus, QueryPagination (cursor ModelId), SearchPattern, Category; component props extend IsAuthenticated
├── constants.ts                  # EMPTY_LIST_LENGTH, ABORT_TIMEOUT_MS
├── components/                   # Shared/generic components
│   ├── form/
│   │   ├── field-errors.tsx
│   │   ├── form-error.tsx
│   │   ├── form-field.tsx        # Shared labeled field + errors (auth ViewTransitions)
│   │   ├── input.tsx
│   │   ├── label.tsx
│   │   └── reset-button.tsx
│   ├── pending-button.tsx        # Pending/busy button (aria-busy, srOnly children)
│   ├── inline-error-fallback.tsx # Shared retry error fallback (navbar / categories)
│   ├── nav-link/
│   │   ├── nav-link-list-item.tsx
│   │   ├── nav-link-skeleton.tsx
│   │   ├── nav-link.tsx          # Suspense-wrapped link with active state (prefetch off by default)
│   │   └── types.ts
│   ├── navbar/                   # Sticky header (compact logo below xs; nav split at sm)
│   │   ├── navbar.tsx            # Orchestrator: logo, offline, desktop nav, mobile popover
│   │   ├── constants.ts
│   │   ├── types.ts
│   │   ├── navbar-logo.tsx
│   │   ├── desktop-nav.tsx
│   │   ├── mobile-menu-button.tsx
│   │   ├── mobile-nav-popover.tsx
│   │   ├── mobile-nav-link.tsx   # Mobile row link; closePopoverOnClick by default
│   │   ├── mobile-nav-auth.tsx
│   │   ├── navbar-auth-slot.tsx
│   │   ├── avatar.tsx
│   │   ├── auth-buttons.tsx
│   │   ├── auth-buttons-skeleton.tsx
│   │   └── sign-in-nav-link.tsx
│   ├── pagination/               # Shared pager UI (`cursor` / `direction` via nuqs)
│   │   ├── pagination.tsx        # Prev/next; `addTransitionType` forwards/backwards
│   │   ├── pagination-button.tsx
│   │   ├── pagination-limit-control.tsx
│   │   ├── pagination-page-control.tsx
│   │   ├── pagination-page-slice.tsx  # PageSlice ViewTransition wrapper
│   │   └── pagination-skeleton.tsx
│   ├── offline-indicator.tsx     # OfflineBanner via next/offline useOffline
│   ├── nuqs/
│   │   └── nuqs-adapter-boundary.tsx  # Suspense + NuqsAdapter for listing routes
│   ├── button-recipe.ts          # Panda CVA recipe for Button variants
│   ├── button.tsx
│   ├── generic-component.tsx
│   ├── not-found/
│   │   ├── unsuccessful-state-list-item.tsx
│   │   └── unsuccessful-state.tsx
│   ├── pill.tsx
│   ├── scroll-progress.tsx
│   ├── skeleton.tsx              # Shared shimmer skeleton (CSS vars / color props)
│   ├── skeleton-enter.ts         # Shared enter transition for skeleton / pending spinner
│   ├── search-input/
│   │   ├── search-input.tsx
│   │   ├── search-input-transition.tsx
│   │   └── search-input-skeleton.tsx
│   ├── suspend.tsx
│   └── top-link.tsx
├── db/
│   ├── schema/
│   │   ├── auth.ts
│   │   ├── likes.ts
│   │   ├── models.ts             # uuidv7 `id` PK + unique `slug`; keyset indexes
│   │   ├── relations.ts
│   │   └── index.ts
│   ├── migrations/               # Drizzle SQL migrations (drizzle-kit generate)
│   ├── seed-data/
│   │   └── models.ts
│   ├── brands.ts                 # CategorySlug / ModelSlug / ModelId / User; `parseModelId`
│   ├── categories.ts             # CATEGORIES constant (source of truth for enum values)
│   ├── create-id.ts              # uuidv7 ModelId factory
│   ├── seed.ts
│   ├── drop-tables.ts
│   └── index.ts
├── lib/
│   ├── api.ts
│   ├── auth.cli.config.ts        # CLI-only Better Auth config for `auth:generate` (no secrets)
│   ├── auth/                     # Session kernel (instance, getUser, types, HasAuth, sign-out)
│   │   ├── index.ts              # Runtime Better Auth (`server-only`; secrets + plugins)
│   │   ├── get-user.ts
│   │   ├── types.ts
│   │   ├── auth-types.ts
│   │   ├── has-auth.tsx
│   │   └── sign-out-action.ts
│   ├── pagination/               # Cursor pagination kit (parsers, schema, DAL, types)
│   │   ├── constants.ts          # DEFAULT_CURSOR / DEFAULT_DIRECTION / LIMITS
│   │   ├── parse-cursor.ts       # URL cursor → branded ModelId
│   │   ├── schema.ts
│   │   ├── search-params.ts      # query / cursor / direction / limit parsers (no sort)
│   │   ├── types.ts
│   │   ├── dal/
│   │   │   └── paginate-items.ts
│   │   └── utils/
│   │       ├── create-keyset-cursor.ts
│   │       ├── to-effective-pagination.ts
│   │       ├── to-pagination.ts  # Slice `limit+1`; hasNext/hasPrevious from cursor
│   │       └── to-paginated-result.ts
│   ├── date.ts
│   ├── hero-image.ts
│   ├── placeholder-image.ts
│   ├── slugify.ts                # MODEL_SLUGIFY_OPTIONS + SlugifyOptions
│   ├── safe-action.ts            # NSA clients + formDataInput (server-only)
│   ├── safe-action-form.ts       # lastNonSecretFormValue for useStateAction (client)
│   └── url.ts                    # Shared URL helpers (nuqs defaultOptions)
├── types/
│   └── index.ts
├── utils/
│   ├── cache-invalidation.ts
│   ├── sanitise-name.ts
│   ├── try-catch.ts
│   └── with-abort.ts             # Combine cache/timeout AbortSignals; withAbort helper
├── global.d.ts
└── proxy.ts

🏗️ Architecture Overview

Feature-Based Organization

The project follows a feature-based architecture where related functionality is co-located:

  • features/models/: Model listing, detail, and search components, queries, and DAL
  • features/models/back-link/: Detail-page back link + allowlisted from search-param helpers
  • features/models/listing/: Listing canonical URLs + path allowlist
  • features/models/sort/: Sort URL state (sort nuqs param), branded types, and Drizzle orderBy mapping
  • features/models/likes/: Like toggle action, DAL, queries, hooks, and heart-button UI
  • features/categories/: All category-related components and data queries
  • features/auth/: Sign-in / sign-up / GitHub NSA stateActions and page UI
  • lib/auth/: Session kernel (getUser, types, HasAuth, sign-out)
  • lib/safe-action.ts: next-safe-action clients (actionClient, authActionClient, formDataInput)
  • lib/safe-action-form.ts: Client helper to echo last non-secret form values
  • lib/pagination/: Cursor pagination parsers (cursor/direction/limit), keyset helpers, DAL, and types
  • components/pagination/: Shared pager UI (PaginationPageSlice + addTransitionType forwards/backwards)
  • db/create-id.ts: uuidv7 ModelId factory for model PK and listing cursor
  • components/: Shared components used across features (including navigation)

Directory Conventions

  • _ prefix: Private folders that are not part of Next.js routing
  • features/: Feature-based modules with their own components and queries
  • components/: Shared/generic components used across features
  • db/categories.ts: Source-of-truth category list; drives SQLite category slug enum values and Valibot branded slugs in db/brands.ts
  • db/brands.ts: Valibot brands for CategorySlug, ModelSlug, ModelId (uuidv7), User
  • db/seed-data/: Model seed data only (models.ts; id assigned at insert)

Performance Optimizations

  • NuqsAdapterBoundary: NuqsAdapter wraps entire 3d-models tree
  • Font Loading: Variable fonts are used as 3+ font weights are used
  • Error Handling: Centralized tryCatch utility for consistent error handling across database queries
  • Cache Components: Uses "use cache", "use cache: remote", and "use cache: private" directives for persistent caching; React cache() is used only for functions called multiple times in the same render pass (e.g., getModelBySlug and getCategoryBySlug called in both generateMetadata and page components)
  • Type Safety: Maybe<T> for nullable query results; UserAuthState discriminated union ({ isAuthenticated: true, user } | { isAuthenticated: false }) from getUser() and HasAuth; shared IsAuthenticated interface extended by models/pagination props; component-specific props co-located next to components where not reused
  • Query Builder: Migrated to Drizzle ORM RQBv2 for simple relational queries (db.query.tableName.findMany/findFirst) with object-based where clauses; complex queries and mutations remain on SQL builder
  • Error Recovery: Error boundaries with error.tsx for failed queries (results, category pages, and model detail pages) with built-in reset() retry functionality and helpful error guidance
  • Database Query Separation: Listing fetches limit+1 rows via exclusive keyset; toPagination / transformToPaginatedResult slice the extra row and set hasNextPage / hasPreviousPage (no COUNT(*))
  • View Transitions: Pager addTransitionType forwards/backwards drives PaginationPageSlice enter/exit (fade + slide); --slide-distance is responsive (100vw-based on small screens, compact from md). Search/sort/limit do not use pager types
  • Abortable listing fetches: search-models combines Next cache signal + AbortSignal.timeout(ABORT_TIMEOUT_MS) via utils/with-abort.ts so cancelled navigations drop DB awaits sooner
  • Platform timeout: Root layout exports maxDuration = 45 (literal) so Vercel/Next can apply a hard execution ceiling

🚀 Getting Started

Prerequisites

  • Bun for package management, tests, and database scripts
  • A current Node.js LTS (used by dev / next build / next start until bun --bun is re-enabled for those scripts; see Next.js runtime note)
  • Turso database (TURSO_DATABASE_URL + TURSO_DATABASE_AUTH)
  • Optional: Bitwarden Secrets Manager machine account token if you use bitwarden() resolvers in .env.schema (see docs/VARLOCK.md)

Installation

  1. Clone the repository

    git clone <repository-url>
    cd 3dmodels
  2. Install dependencies

    bun install

    This runs the prepare lifecycle script (panda build to generate styled-system/, plus Husky). If codegen ever needs a manual rerun: bunx panda build.

  3. Environment Setup Configuration is defined in .env.schema (Varlock). Copy it to .env and fill in values, or use literal strings in place of bitwarden("…") UUIDs for local development. Typical variables:

    # Bootstrap (Bitwarden resolvers in .env.schema)
    BITWARDEN_ACCESS_TOKEN="your-machine-account-token"
    
    NEXT_PUBLIC_SITE_URL="http://localhost:3000"
    
    BETTER_AUTH_SECRET="your-secret-key-here-change-this-in-production"
    
    GITHUB_CLIENT_ID="your-github-oauth-client-id"
    GITHUB_CLIENT_SECRET="your-github-oauth-client-secret"
    
    TURSO_DATABASE_URL="libsql://your-db.turso.io"
    TURSO_DATABASE_AUTH="your-turso-auth-token"

    Run bun run env:typegen after changing .env.schema to refresh src/env.d.ts. Typed access uses import { ENV } from "varlock/env". Tests use committed .env.test (static fixtures; no secret fetch). See docs/VARLOCK.md and docs/AUTH_SETUP.md for Bitwarden, Bun, and Vercel notes.

  4. Database Setup Scripts use varlock run -- so Drizzle and seed commands receive resolved env (see package.json):

    bun run db:push
    bun run db:seed

    Alternatively, migrations (SQL and meta/ snapshots are written to src/db/migrations/ when you run generate; clones may use db:push only until migrations exist):

    bun run db:generate
    bun run db:migrate
    bun run db:seed

    For one-off Drizzle CLI use without the db:* scripts, use the same pattern as package.json (for example varlock run -- bun x drizzle-kit push).

  5. Start the development server

    bun run dev

    Open http://localhost:3000 to view the application.

📊 Database Schema

Categories Table

  • id: Primary key (auto-increment)
  • displayName: Human-readable category name
  • slug: Category slug text enum (unique); values defined in src/db/categories.ts

Models Table

  • id: Primary key (uuidv7 ModelId, listing cursor; src/db/create-id.ts)
  • slug: Unique public key (text, auto-generated from name); detail path /3d-models/{slug}
  • name: Model name (unique)
  • description: Model description
  • likes: Number of likes (counter)
  • image: Image URL
  • categorySlug: Foreign key to categories.slug
  • userId: Foreign key to user.id (cascade delete)
  • dateAdded: Timestamp when model was added
  • Indexes: (date_added, id), (likes, id), (name, id) for keyset seeks
  • Migration 20260916193419_models_uuidv7_pk rebuilds models with uuidv7 id, copies existing rows, keeps likes (slug FK)

Likes Table

  • id: Primary key (auto-increment)
  • userId: Foreign key to users.id (cascade delete)
  • modelSlug: Foreign key to models.slug (cascade delete)
  • createdAt: Timestamp when like was created
  • Unique constraint on (userId, modelSlug) pair

Authentication Tables (Better Auth)

  • user: User accounts with email/password and OAuth support
  • account: OAuth / credential accounts (issuer column dropped in Better Auth 1.7.3)
  • session: User sessions with cookie caching
  • verification: Email verification tokens

🗄️ Database Operations

Available Scripts

  • bun run auth:generate — Regenerate Better Auth Drizzle schema (src/db/schema/auth.ts from src/lib/auth.cli.config.ts)
  • bun run db:generate — Generate migrations (varlock run -- bun x drizzle-kit generate)
  • bun run db:migrate — Run migrations (varlock run -- bun x drizzle-kit migrate)
  • bun run db:push — Push schema (varlock run -- bun x drizzle-kit push --force)
  • bun run db:studio — Drizzle Studio (varlock run -- bun x drizzle-kit studio)
  • bun run db:seed — Wipe and reseed models/likes (requires existing users; uuidv7 ModelId at insert). Auth tables stay. Not run by db:migrate.
  • bun run db:drop — Drop all tables (development reset)

Database Relations

The application uses Drizzle ORM 1.0.0-rc.4 with defineRelations for type-safe relations:

  • Relations defined using the v1/rc syntax with r.one() and r.many() helpers
  • Relation names avoid conflicts with column names (e.g., modelLikes instead of likes to avoid conflict with models.likes column)
  • All relations exported from schema/relations.ts and included in the database schema

Query Builder (RQBv2)

The application uses Drizzle ORM's Relational Query Builder v2 (RQBv2) for type-safe relational queries:

  • Read queries: All read queries use RQBv2 syntax (db.query.tableName.findMany(), db.query.tableName.findFirst()) with object-based where clauses
  • Listing / filter queries: Listing search uses SQL builder (and(), or(), like() + COLLATE NOCASE) since SQLite has no ilike; exclusive keyset on (sortCol, ModelId) via create-keyset-cursor.ts
  • Mutations: Insert, update, and delete operations use the SQL builder syntax (mutations not yet available in RQBv2)
  • Hybrid approach: The codebase uses a hybrid strategy - RQBv2 object syntax for simple reads, SQL builder for listing keyset/where and mutations
  • Query organization: Model listing is get-models-list.ts (limit+1 keyset, no COUNT(*)) plus build-models-where.ts for search/category filters, composed in DAL (get-models.ts, search-models.ts). List ordering and keyset column/dir come from features/models/sort/order-for-sort.ts via the sort search param; equal likes / equal dateAdded order by ModelId
  • Better Auth adapter: Uses @better-auth/drizzle-adapter/relations-v2 with advanced.database.joins (lib/auth/index.ts runtime, lib/auth.cli.config.ts for generate); provider: "sqlite"; mounted on ElysiaJS at /api/[[...slugs]]/route.ts with basePath /api/auth; OpenAPI via better-auth-openapi.ts

Cache Components

The application uses Next.js Cache Components for optimal performance:

  • Static content is pre-rendered at build time
  • Dynamic content (like authentication state) is rendered at request time
  • Server components use connection() to opt into dynamic rendering when needed (e.g. getUser())
  • Cache invalidation handled by cacheTag utilities
  • Error handling with error.tsx error boundaries for failed queries (categories, results, and category pages with built-in reset() retry functionality)
  • Loading states with loading.tsx for results and category pages

Caching Strategy

The application uses Next.js Cache Components with granular cache tags for efficient invalidation:

  • Models: Cached with models, model-{slug}, and models-category-{slug} tags
  • Categories: Cached at component level with categories tag and cacheLife("max") for pre-rendered HTML output
  • Cache Life: Hours profile for most queries (5 min stale, 1 hour revalidate, 1 day expire), max for static categories (component-level caching)
  • Query Functions: Unified getModels() uses searchModels() for search (optional query), category filter, sort, and cursor pagination. Helper getModelsList supports optional search/category; sort maps through orderByForSort / sortColumnForSort. Cursor parsed via parsePaginationCursor (uuidv7 ModelId)
  • Like Status: like-status.ts queries use "use cache: private" for user-specific like status (cached on device)
  • Model Lists: get-models.ts adds hasLiked per model after a single batched like query for the current PageSlice
  • Invalidation: invalidateAllModels() in utils/cache-invalidation.ts (updateTag("models")) — simple broad invalidation on like/unlike so counts and sort=popular stay correct; longer-term content/likes split documented in docs/MODEL_CACHE_SPLIT.md
  • Optimistic Updates: Heart button uses NSA useOptimisticStateAction for immediate UI feedback with server state synchronization via form actions

🎨 Styling & Components

Design System

  • Tokens & utilities: Panda CSS 2 theme/ define* modules assembled by panda.config.ts (@pandacss/preset-base, @pandacss/preset-panda, @pandacss/preset-typography); orange accent and shared patterns (between, hoverShadow) live in theme/; treeshakeDesignSystem enabled; local keyframes() from @styled-system/css for scroll/view-timeline animations not meant for the global theme
  • Typography: Albert Sans + Montserrat Alternates via next/font in root layout; heading font applied in Panda globalCss
  • Layout & spacing: Panda css() / layout patterns (e.g., grid for model grids in src/features/models/components/models-grid.styles.ts)
  • View transitions: Colocated with components via React ViewTransition + Panda view-transition helpers (global VT CSS removed from index.css)
  • Responsive: Mobile-first breakpoints via Panda conditions and component styles

Key Components

Feature Components

  • features/models/components/model-card - Individual model display card (container-query split + subgrid via model-card.styles; local view-timeline keyframes() on touch)
  • features/models/components/model-card-skeleton - Loading skeleton for model cards
  • features/models/components/model-detail - Detailed model view (model-detail.styles container queries)
  • features/models/back-link/model-back-link - Server back link restoring allowlisted listing from (runtime prefetch={true}; arrowRecipe left chevron micro-interactions)
  • features/models/back-link/from-search-params - modelDetailHref / resolveBackHref with slugify-stable slug checks
  • features/models/components/models-grid - Grid layout for model cards (embeds from on detail links)
  • features/models/components/models-grid-header - Listing header: search input, query-aware title, sort controls
  • features/models/components/models-grid-title - Client title via nuqs (Results for "{query}" or fallback category / default title)
  • features/models/components/models-not-found - Empty search state with the active query in the subheading
  • features/models/components/models-sort-controls - Client fieldset of SortOption radios for A-Z / Popular / Recent (useSortQuery)
  • features/models/components/models-sort-controls-skeleton - Loading skeleton for sort controls
  • features/models/components/models-view - Shared server shell: header outside Suspense, ModelsViewResult switch, async inner awaits getModels; success wraps grid + pager in PaginationPageSlice
  • features/models/sort/components/sort-option - Single sort radio + label pill
  • features/models/sort/hooks/use-sort-query - nuqs hook for sort with pending state; resets cursor/direction on sort change
  • features/models/sort/order-for-sort - Maps sort brand to Drizzle orderBy plus keyset column/dir (tie-break ModelId)
  • features/models/listing/listing-canonical - Canonical path serializer for listing SEO (query, cursor, direction, limit, sort)
  • features/models/listing/listing-path - Allowlisted listing Route validation (/3d-models, category listings)
  • components/pagination/pagination - Cursor pager via nuqs cursor/direction; addTransitionType forwards/backwards
  • components/pagination/pagination-button - Page control button (group for arrow micro-interactions)
  • components/pagination/pagination-limit-control - Per-page limit via customizable <select> (appearance: base-select, selectedcontent, picker transitions)
  • components/pagination/pagination-page-slice - PageSlice ViewTransition wrapper; pager types only (not search/sort/limit)
  • components/pagination/pagination-page-control - Prev/next buttons with aria-label and arrowRecipe chevrons
  • lib/pagination/parse-cursor - URL cursor → branded ModelId
  • lib/pagination/utils/create-keyset-cursor - Exclusive (sortCol, ModelId) predicate
  • lib/pagination/utils/to-pagination - Slice limit+1; hasNextPage/hasPreviousPage from cursor + extra row
  • features/models/likes/components/heart-button-client - Client like button (useHeartLike, optimistic state) or guest /signin Link + hint popover; colors from heartButtonRecipe({ visual, guest })
  • features/models/likes/components/likes-count-transition - Wraps like count with ViewTransition update names for increase/decrease
  • features/models/likes/components/heart-button-server - Server component for detail pages (resolves like status server-side)
  • features/models/likes/components/heart-button-skeleton - Loading skeleton for heart button
  • features/models/likes/hooks/use-heart-like - Client hook wrapping NSA useOptimisticStateAction (toggleAction, optimistic state, visualState)
  • components/search-input/search-input - Model search with nuqs URL state (mounted in ModelsGridHeader); Enter flushes current input value; resets cursor/direction; search-input-transition for view transitions
  • features/categories/components/categories-nav - Category filtering sidebar (server component)
  • features/categories/components/categories-block-transition - View transition wrapper for category listing blocks
  • app/3d-models/layout - Sticky categories aside: md overflow-y + header clearance; local keyframes() scroll mask on the horizontal strip
  • app/3d-models/@categories/error.tsx - Error boundary for categories with built-in retry functionality
  • app/3d-models/@results/error.tsx - Error boundary for search results with retry and error guidance
  • app/3d-models/@results/loading.tsx - Loading state for search results
  • app/3d-models/categories/[categoryName]/error.tsx - Error boundary for category pages with retry and error guidance
  • app/3d-models/categories/[categoryName]/loading.tsx - Loading state for category pages
  • app/3d-models/[slug]/error.tsx - Error boundary for model detail pages with retry and error guidance

Navigation Components

  • app/@navbar/default - Parallel route delegating to shared Navbar
  • app/@navbar/error.tsx - Error boundary for navbar with retry functionality
  • app/@footer/default - Footer parallel route with copyright
  • components/navbar/navbar - Thin orchestrator: NavbarLogo, OfflineBanner, DesktopNav, MobileMenuButton, MobileNavPopover (sm breakpoint); local keyframes() scroll-driven header elevate
  • components/navbar/mobile-nav-link - Mobile popover row; closes ancestor popover on navigate
  • components/offline-indicator - OfflineBanner using Next.js useOffline, with persistent reserved navbar placement and container-responsive detail (hidden when online)
  • components/nav-link/nav-link - NavLink with Suspense fallback, active state (includes or endsWith), border position (bottom or left); prefetch={false} by default to reduce network noise (opt-in per link, e.g. model back link)
  • components/nav-link/nav-link-skeleton - Width-matched skeleton for NavLink Suspense fallback
  • components/nav-link/nav-link-list-item - li + NavLink wrapper
  • components/nuqs/nuqs-adapter-boundary - Suspense + NuqsAdapter for the 3d-models layout
  • components/top-link - Top-of-page control used in layouts
  • components/navbar/auth-buttons - Sign-out control wrapping avatar (authenticated navbar slot)
  • components/navbar/auth-buttons-skeleton - Navbar auth slot loading state
  • components/navbar/sign-in-nav-link - Icon-only sign-in link for unauthenticated navbar slot
  • features/auth/components/auth-card - Card shell for sign-in/sign-up pages
  • features/auth/components/auth-footer-link - Footer link between auth screens
  • components/navbar/avatar - Avatar image with fallback

Shared Components

  • components/arrow-recipe - Panda CVA for directional arrow icons (left/right, compact/default distance, size; group hover/focus/active)
  • components/button - Shared button styled with Panda variants (primary, outline, brandOutline, ghost) and densities
  • components/button-recipe - Button CVA recipe (brandOutline for idle sort; neutral outline for GitHub sign-in)
  • components/form/input - Text input with consistent field styling
  • components/form/label - Accessible labels for form fields
  • components/form/form-field - Shared labeled field shell with field errors and shared ViewTransition names for auth forms
  • components/pending-button - Pending/busy button (aria-busy, srOnly children, spinner)
  • components/inline-error-fallback - Shared retry error fallback (message + retry) for slim parallel-route error.tsx files
  • components/form/reset-button - Reset control for forms
  • components/form/field-errors - Field-level error display component with ViewTransition support
  • components/form/form-error - Form-level error display component with ViewTransition support
  • components/not-found/unsuccessful-state - Unified component for not-found and error states with conditional styling based on isError prop
  • components/not-found/unsuccessful-state-list-item - List item component for unsuccessful state suggestions
  • components/pill - Small label component
  • components/scroll-progress - Top-of-page reading progress indicator; local keyframes() scroll-timeline fill/reveal
  • components/skeleton - Shared shimmer skeleton (color / highlightColor → CSS vars; layout via className)
  • components/suspend - Suspense helper component
  • components/generic-component - Generic wrapper for collections

Authentication & Data Access

  • lib/auth.cli.config - CLI-only Better Auth config for schema generate (no secrets)
  • lib/auth - Runtime Better Auth (server-only; secrets, OAuth, cookies, OpenAPI)
  • lib/auth/get-user - User query with React.cache() (returns UserAuthState from session)
  • lib/auth/has-auth - Renders children(auth) with UserAuthState; HasAuthSuspense wraps in Suspend
  • lib/auth/sign-out-action - Sign-out server action (actionClient.stateAction)
  • lib/safe-action - next-safe-action clients (actionClient, authActionClient, formDataInput; server-only)
  • lib/safe-action-form - lastNonSecretFormValue for useStateAction last input (client; skips password fields)
  • lib/slugify - Shared MODEL_SLUGIFY_OPTIONS / SlugifyOptions for seed + isModelSlug
  • utils/with-abort - Combine AbortSignals and race promises against abort
  • features/auth/actions - Sign-in, GitHub OAuth, and sign-up NSA stateActions (Valibot + formDataInput)
  • features/auth/constants - Validation constants (password length, email length, name length limits)
  • features/auth/components/auth-form - Shared sign-in/sign-up form (useStateAction + lastNonSecretFormValue)
  • features/auth/components/sign-in-button - GitHub OAuth form (signInGithubAction + useStateAction)
  • components/form/field-errors - Field errors from NSA flattened validationErrors.fieldErrors
  • components/form/form-error - Form-level serverError from NSA result

🔧 Development

Code Quality Tools

  • Biome / Ultracite: Linting and formatting (see biome.json and AGENTS.md)
  • Fallow: Dead-code / health / duplication plus boundaries.preset: "bulletproof" (.fallowrc.json); health maxCyclomatic / maxCognitive 12, maxCrap 30; fallow audit on Cursor stop hook, before commit/push, and on PRs (.github/workflows/fallow.yml)
  • React Doctor: React/Next.js diagnostics on pull requests and main pushes (.github/workflows/react-doctor.yml); pre-commit via lint-staged (bun run type, bun run react-doctor:staged); full local run with bun run react-doctor
  • GitHub Actions: Pin uses: to a full SHA plus same-line # vX.Y.Z; Dependabot weekly (github-actions in .github/dependabot.yml) bumps SHA and comment together
  • Cursor hooks: .cursor/hooks.json — fast afterFileEdit (Ultracite with --skip=correctness/noUnusedImports, test:affected); heavier stop (full fix, fallow audit, full test)
  • TypeScript 7: Static type checking via tsc (bun run type); Next uses experimental.useTypeScriptCli

Available Scripts

  • prepare (automatic on bun install) — Panda styled-system/ build and Husky setup
  • bun run dev - Start development server (Turbopack; Node runtime — see Next.js runtime note)
  • bun run dev:inspect - Start development server with inspector (bun --bun; expect CLIENT_HOOK_DYNAMIC noise — see runtime note)
  • bun run next:upgrade - Upgrade Next.js to latest version (bun --bun)
  • bun run next:analyze - Analyze Next.js bundle (next experimental-analyze on Node via Varlock, same as build). Pass-through: bun run next:analyze -- --output --baseline-name pre-nsa. --output writes .next/diagnostics/analyze without serving; --baseline-name labels the snapshot so a later run can compare in the analyzer UI history. Omit --output to open the UI (default port 4000).
  • bun run build - Build for production (Node runtime via bun varlock run -- next build)
  • bun run build:debug - Build with debug prerender (bun --bun; same CLIENT_HOOK_DYNAMIC leak as runtime note)
  • bun run start - Start production server (Node runtime via bun run next start)
  • bun run test - Run tests (Bun test runner; VARLOCK_ENV=test loads .env.test — no Bitwarden)
  • bun run test:affected - Run tests affected by git changes (bun test --changed --pass-with-no-tests; Cursor afterFileEdit)
  • bun run test:watch - Run tests in watch mode
  • bun run test:unit - Run unit tests
  • bun run test:components - Run component tests
  • bun run test:integration - Run integration tests
  • bunfig.tomlenv = false (Varlock owns .env* resolution); minimumReleaseAge (3 days) with Next/SWC/Panda excludes; test preload order: tests/setup/varlock-test-env.tsvarlock/auto-loadtests/setup/test-preload.ts (Happy DOM + server-only stub); tests/setup/next-mocks.ts stubs Next navigation; listing nuqs tests use withListingNuqsTestingAdapter from tests/setup/nuqs-testing.ts (nuqs/adapters/testing + defaultOptions); integration DB helpers in tests/setup/db-test.ts use Drizzle 1.0 { client } config
  • bun run test:e2e - Run Playwright E2E tests
  • bun run e2e:open - Open Playwright UI
  • bun run e2e:codegen - Playwright codegen (localhost:3000)
  • bun run type - Run Next typegen then tsc type checking
  • bun run typegen - Generate Next.js routes (next typegen)
  • bun run env:typegen - Regenerate src/env.d.ts from .env.schema (Varlock)
  • bun run auth:generate - Regenerate Better Auth Drizzle schema from auth.cli.config.ts
  • bun run db:generate - Generate Drizzle migrations
  • bun run db:migrate - Run Drizzle migrations
  • bun run db:push - Push schema directly to database
  • bun run db:studio - Open Drizzle Studio
  • bun run db:seed - Seed database with initial data
  • bun run db:drop - Drop all tables (development reset)
  • bun run fix - Fix linting/formatting issues with Ultracite/Biome
  • bun run check - Check linting rules with Ultracite/Biome
  • bun run doctor - Run Ultracite doctor diagnostics
  • bun run ultracite:upgrade - Upgrade Ultracite configuration (ultracite@7.11.1)
  • bun run react-doctor - Run React Doctor on the repo (doctor.config.ts)
  • bun run react-doctor:staged - Run React Doctor on staged files only (lint-staged / pre-commit)

Code Style

The project follows a consistent coding style with:

  • ES modules (import/export syntax)
  • TypeScript for type safety
  • Panda CSS for styling (css recipes, semantic tokens)
  • Feature-based organization
  • Component-specific type definitions
  • Proper error handling and logging

🚀 Deployment

Vercel (Recommended)

  1. Connect your repository to Vercel
  2. Set environment variables in Vercel dashboard
  3. Deploy automatically on push to main branch
  4. Runtime note: Bun is the install runtime (vercel.json bunVersion 1.4.x, installCommand: bun install). Leave the build on default Next (next build on Node) — do not set buildCommand to bun --bun run next build until oven-sh/bun#39847 / vercel/next.js#97663 land. Cache Components still require the Node runtime. Git auto-deploys are currently off (git.deploymentEnabled: false).

Environment Variables

Mirror .env.schema: NEXT_PUBLIC_SITE_URL, BETTER_AUTH_SECRET, GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, TURSO_DATABASE_URL, TURSO_DATABASE_AUTH, and BITWARDEN_ACCESS_TOKEN when using bitwarden() resolvers. Varlock validates at runtime; types live in src/env.d.ts. See docs/VARLOCK.md for Vercel and Bitwarden.

📝 Data Management

Adding New Models

  1. Update src/db/seed-data/models.ts with new model data (note: userId and likes are omitted from seed data)
  2. Run bun run db:seed to update the database (requires existing users in the database)

Adding New Categories

  1. Add the category to src/db/categories.ts (updates the category slug enum source of truth)
  2. Run bun run db:generate then bun run db:migrate (or bun run db:push in development)
  3. Run bun run db:seed to update the database

Cache Management

  • Use centralized cache invalidation in utils/cache-invalidation.ts
  • Function: invalidateAllModels() — expires every entry tagged "models" via updateTag("models")
  • toggleLike calls invalidateAllModels() so list counts and sort=popular stay fresh (simple correctness choice for now)
  • Longer-term split of stable content vs likes (and popular-order tags): see docs/MODEL_CACHE_SPLIT.md
  • Session cache uses "use cache: private" directive with cacheTag("session") for responsive auth state
  • Like status uses features/models/likes/queries/like-status.ts with "use cache: private" for user-specific cache

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes following the feature-based architecture
  4. Run tests and linting
  5. Submit a pull request

📄 License

This project is licensed under the MIT License.

🆘 Support

For support and questions:

  • Check the documentation
  • Review existing issues
  • Create a new issue with detailed information