A modern web application for browsing and discovering 3D models, built with Next.js, TypeScript, and Drizzle ORM.
- Framework: Next.js 16.4.0-canary.2 with App Router, Cache Components, React Compiler, typed routes (
typedRoutes), experimentaluseOffline, and rootmaxDuration = 45(platform hard kill ceiling) - Language: TypeScript 7.0.2 with React 19.3 canary (
19.3.0-canary-eb8feb71-20260814, aged forbunfigminimumReleaseAge) - Styling: Panda CSS 2.0.0-beta.14 (
@pandacss/dev,@pandacss/preset-base,@pandacss/preset-panda,@pandacss/preset-typography,panda.config.ts); generatedstyled-system/frompanda build(gitignored; run viabun install/prepare); imports use the@styled-system/*path alias (tsconfig.json);@layerstack insrc/app/index.css; view-transition animations colocated via Panda helpers /ViewTransition(duration tokens +groupwhere needed); sharedSkeletonuses shimmer CSS vars (color/highlightColorprops) - Database: Turso (libSQL / SQLite) with Drizzle ORM 1.0.0-rc.4 (
dialect: "turso",@libsql/client) - Authentication: Better Auth 1.7.1 with email/password and GitHub OAuth, cookie caching enabled, ElysiaJS API backend; Drizzle adapter uses
relations-v2with experimental joins (provider: "sqlite"); account identity keyed by(issuer, accountId) - Search Params: nuqs 2.10.0 for type-safe URL state (
query,page,limit,sort);NuqsAdapterBoundarywraps the3d-modelslayout so search works on index and category routes; listing canonical URLs usenuqs/serverloaders/serializers (features/pagination/listing-canonical.ts) withclearOnDefaultfor SEO metadata; model detailfromreturn paths are allowlisted viafeatures/pagination/listing-path.ts - Linting & Formatting: Biome 2.5.9 with Ultracite 7.10.6 presets (
ultracite/biome/core,react,next); React Doctor on PRs and pushes tomain(.github/workflows/react-doctor.yml, 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, fulltest) - Type Checking: TypeScript 7 via
tsc(bun run type/typegen); Next build uses project-localtsc(experimental.useTypeScriptCliinnext.config.ts) because TS 7 has no JS compiler API - Package Manager: Bun (install, tests, Drizzle scripts,
prepare);bunfig.tomlsetsminimumReleaseAge(3 days) - Next.js runtime: Bun is the desired runtime (
bun --bunfor inspect / debug build). Temporarily,dev, productionbuild, andstartrun Next on Node (next dev,bun varlock run -- next build,bun run next start) because Next.js 16 Cache Components +bun --buncan surface spuriousAbortErrorunhandled rejections during prerender. Plan to re-enablebun --bunfor all Next scripts once Bun/Next compatibility improves (see vercel/next.js#87630, oven-sh/bun#26508). - Build Tool: Turbopack for dev and build;
partialPrefetching, experimental view transitions, MCP server, cached navigations,appNewScrollHandler, Turbopack filesystem caches (turbopackFileSystemCacheForDev/ForBuild), andturbopackRustReactCompiler(next.config.ts); env types from Varlock (.env.schema,src/env.d.ts), not NexttypedEnv - Environment: Varlock 1.17.0 with
.env.schema(@currentEnv=$VARLOCK_ENV,@encryptInjectedEnv=forEnv(production, preview)), committed.env.testfixtures for Bun tests (no Bitwarden fetch),@varlock/nextjs-integrationplugin innext.config.ts, optional Bitwarden Secrets Manager via@varlock/bitwarden-plugin(seedocs/VARLOCK.md) - Validation: Varlock for environment; Valibot 1.4.2 for server action and form schemas; model slugs validated via slugify idempotency (
lib/slugify.ts+isModelSlugindb/brands.ts)
- 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
sortsearch param (default A-Z omitted from URL) - Search across listings: Search bar lives in
ModelsGridHeader(index + category routes); grid title showsResults for "{query}"via nuqs when a query is present - Model detail back link: Detail pages restore the prior listing via allowlisted
fromquery (features/models/back-link/) with runtime prefetch under Partial Prefetching - Shimmer skeletons: Shared
Skeletonshimmer (CSS vars /colorprops) for listing and detail loading states - Offline indicator: Navbar
OfflineBannervia Next.js experimentaluseOfflineβ 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 belowxs(480px), with the full wordmark fromxsupward (src/components/navbar/) - Responsive Design: Optimized for desktop, tablet, and mobile devices
- Smooth Page Transitions: View Transitions API with composable fade and slide animations for pagination (full-viewport slide on small screens, compact slide from
md) - 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.3, 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.
Static assets are served from public/ at the repository root (not under src/), including logos, hero images, and public/img/models/*.jpg 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). Panda CSS writes generated files to styled-system/ at the repo root (panda.config.ts β 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.
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
β β βββ 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)
β βββ styles.ts # Shared Panda `css` / pattern exports for app shells
β βββ 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
βββ assets/
β βββ images/ # App-local image assets
βββ features/
β βββ auth/ # Authentication feature
β β βββ actions/ # Server actions
β β β βββ sign-in-action.ts
β β β βββ sign-out-action.ts
β β β βββ sign-up-action.ts # SignUpData type co-located here
β β βββ components/ # Auth components
β β β βββ auth-buttons.tsx
β β β βββ auth-buttons-skeleton.tsx
β β β βββ auth-card.tsx
β β β βββ auth-footer-link.tsx
β β β βββ avatar.tsx # User avatar (GitHub image, fallback icon)
β β β βββ has-auth.tsx # Generic auth component with session provider
β β β βββ sign-in-button.tsx
β β β βββ sign-in-nav-link.tsx # Icon-only sign-in NavLink for navbar
β β βββ auth-types.ts # Shared auth type definitions
β β βββ constants.ts # Auth validation constants
β β βββ queries/
β β β βββ get-user.ts
β β βββ types.ts # IsAuthenticated, UserAuthState discriminated union
β βββ 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-skeleton.tsx
β β β βββ model-detail.tsx
β β β βββ 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, batched likes
β β β βββ search-models.ts # Unified listing/search + abortable cached awaits
β β βββ 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 for alphabetic / popular / recent
β β β βββ 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-server.tsx
β β β β βββ heart-button-skeleton.tsx
β β β β βββ heart-icon.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
β β βββ queries/
β β β βββ build-models-where.ts # Shared SQL where builder for list/count
β β β βββ get-all-model-slugs.ts
β β β βββ get-model-by-slug.ts
β β β βββ get-models-count.ts
β β β βββ get-models-list.ts
β β βββ types.ts # ModelWithLikeStatus, SearchPattern, Category; component props extend IsAuthenticated
β βββ pagination/
β βββ components/
β β βββ pagination-button.tsx
β β βββ pagination-limit-control.tsx
β β βββ pagination-offset-transition.tsx
β β βββ pagination-page-control.tsx
β β βββ pagination-skeleton.tsx
β β βββ pagination-summary.tsx
β β βββ pagination.tsx
β βββ dal/
β β βββ paginate-items.ts
β βββ hooks/
β β βββ use-pagination-query.ts
β βββ utils/
β β βββ to-paginated-result.ts
β βββ listing-canonical.ts
β βββ listing-path.ts # Allowlisted listing href β Route (open-redirect safe)
β βββ pagination-search-params.ts
β βββ constants.ts
β βββ types.ts # PaginatedResult, PaginationMetadataObject; component props co-located in components/
βββ 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
β β βββ submit-button.tsx
β βββ 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
β β βββ navbar.constants.ts # MOBILE_NAVIGATION_ID, typed PRIMARY_NAV_LINKS
β β βββ navbar.types.ts # NavbarNavLinkConfig (typed Route + NavLink props)
β β βββ navbar-shell.styles.ts # Header shell + :has() hamburger-open animation
β β βββ 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
β βββ 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)
β βββ 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 # categories + models tables (sqlite text enum for category slug)
β β βββ relations.ts
β β βββ index.ts
β βββ migrations/ # Drizzle SQL migrations (drizzle-kit generate)
β βββ seed-data/
β β βββ models.ts
β βββ brands.ts # CategorySlug / ModelSlug / User branded types; `isModelSlug`
β βββ categories.ts # CATEGORIES constant (source of truth for enum values)
β βββ 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.ts # Runtime Better Auth (`server-only`; secrets + plugins)
β βββ auth-client.ts
β βββ date.ts
β βββ hero-image.ts
β βββ slugify.ts # MODEL_SLUGIFY_OPTIONS + SlugifyOptions
β βββ url.ts # Shared URL helpers (nuqs defaultOptions)
βββ types/
β βββ index.ts
βββ utils/
β βββ cache-invalidation.ts
β βββ sanitise-name.ts
β βββ to-action-state/ # Server action result helpers
β β βββ form-data-to-safe-payload.ts # Safe FormData β client payload (#38)
β β βββ to-action-state.ts
β β βββ types.ts
β βββ try-catch.ts
β βββ with-abort.ts # Combine cache/timeout AbortSignals; withAbort helper
βββ global.d.ts
βββ proxy.ts
The project follows a feature-based architecture where related functionality is co-located:
features/models/: Model listing, detail, and search components, queries, and DALfeatures/models/back-link/: Detail-page back link + allowlistedfromsearch-param helpersfeatures/models/sort/: Sort URL state (sortnuqs param), branded types, and DrizzleorderBymappingfeatures/models/likes/: Like toggle action, DAL, queries, hooks, and heart-button UIfeatures/categories/: All category-related components and data queriesfeatures/pagination/: Pagination utilities, types, listing canonical/path allowlist, and components shared across featuresfeatures/auth/: Authentication actions, components, queries, and typescomponents/: Shared components used across features (including navigation)
_prefix: Private folders that are not part of Next.js routingfeatures/: Feature-based modules with their own components and queriescomponents/: Shared/generic components used across featuresdb/categories.ts: Source-of-truth category list; drives SQLite category slug enum values and Valibot branded slugs indb/brands.tsdb/seed-data/: Model seed data only (models.ts)
- NuqsAdapterBoundary:
NuqsAdapterwraps entire 3d-models tree - Font Loading: Variable fonts are used as 3+ font weights are used
- Error Handling: Centralized
tryCatchutility for consistent error handling across database queries - Cache Components: Uses
"use cache","use cache: remote", and"use cache: private"directives for persistent caching; Reactcache()is used only for functions called multiple times in the same render pass (e.g.,getModelBySlugandgetCategoryBySlugcalled in bothgenerateMetadataand page components) - Type Safety:
Maybe<T>for nullable query results;UserAuthStatediscriminated union ({ isAuthenticated: true, user }|{ isAuthenticated: false }) fromgetUser()andHasAuth; sharedIsAuthenticatedinterface 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-basedwhereclauses; complex queries and mutations remain on SQL builder - Error Recovery: Error boundaries with
error.tsxfor failed queries (results, category pages, and model detail pages) with built-inreset()retry functionality and helpful error guidance - Database Query Separation: Database queries return raw
DatabaseQueryResult<T>; transformation toPaginatedResult<T>happens in higher-level functions usingtransformToPaginatedResultutility fromfeatures/pagination/utils/ - View Transitions: Composable CSS animations using base fade and slide keyframes with CSS variables for slide distance, enabling smooth directional page transitions (enter-left, exit-left, enter-right, exit-right) for pagination;
--slide-distanceis responsive (100vw-based on small screens, compact frommd) - Abortable listing fetches:
search-modelscombines Next cache signal +AbortSignal.timeout(ABORT_TIMEOUT_MS)viautils/with-abort.tsso cancelled navigations drop DB awaits sooner - Platform timeout: Root layout exports
maxDuration = 45(literal) so Vercel/Next can apply a hard execution ceiling
- Bun for package management, tests, and database scripts
- A current Node.js LTS (used by
next build/next startuntilbun --bunis re-enabled for those scripts) - Turso database (
TURSO_DATABASE_URL+TURSO_DATABASE_AUTH) - Optional: Bitwarden Secrets Manager machine account token if you use
bitwarden()resolvers in.env.schema(seedocs/VARLOCK.md)
-
Clone the repository
git clone <repository-url> cd 3dmodels
-
Install dependencies
bun install
This runs the
preparelifecycle script (panda buildto generatestyled-system/, plus Husky). If codegen ever needs a manual rerun:bunx panda build. -
Environment Setup Configuration is defined in
.env.schema(Varlock). Copy it to.envand fill in values, or use literal strings in place ofbitwarden("β¦")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:typegenafter changing.env.schemato refreshsrc/env.d.ts. Typed access usesimport { ENV } from "varlock/env". Tests use committed.env.test(static fixtures; no secret fetch). Seedocs/VARLOCK.mdanddocs/AUTH_SETUP.mdfor Bitwarden, Bun, and Vercel notes. -
Database Setup Scripts use
varlock run --so Drizzle and seed commands receive resolved env (seepackage.json):bun run db:push bun run db:seed
Alternatively, migrations (SQL and
meta/snapshots are written tosrc/db/migrations/when you run generate; clones may usedb:pushonly 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 aspackage.json(for examplevarlock run -- bun x drizzle-kit push). -
Start the development server
bun run dev
Open http://localhost:3000 to view the application.
id: Primary key (auto-increment)displayName: Human-readable category nameslug: Category slug text enum (unique); values defined insrc/db/categories.ts
slug: Primary key (text, auto-generated from name)name: Model name (unique)description: Model descriptionlikes: Number of likes (counter)image: Image URLcategorySlug: Foreign key to categories.sluguserId: Foreign key to user.id (cascade delete)dateAdded: Timestamp when model was added
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
user: User accounts with email/password and OAuth supportaccount: OAuth / credential accounts keyed by(issuer, accountId)unique indexsession: User sessions with cookie cachingverification: Email verification tokens
bun run auth:generateβ Regenerate Better Auth Drizzle schema (src/db/schema/auth.tsfromsrc/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β Seed database (requires existing users for seeded models)bun run db:dropβ Drop all tables (development reset)
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()andr.many()helpers - Relation names avoid conflicts with column names (e.g.,
modelLikesinstead oflikesto avoid conflict withmodels.likescolumn) - All relations exported from
schema/relations.tsand included in the database schema
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-basedwhereclauses - Count / filter queries: Listing search uses SQL builder (
and(),or(),like()+COLLATE NOCASE) since SQLite has noilike - 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 all read queries (including complex conditions with
AND/ORarrays), SQL builder for count where conditions and mutations - Query organization: Model queries are split into focused functions (
get-models-list.tsfor listing with RQBv2,get-models-count.tsfor counting with SQL builder,build-models-where.tsfor shared filter conditions) and composed in higher-level DAL functions (get-models.ts,search-models.ts). Both helpers support optionalsearchPatternandcategoryparameters; list ordering comes fromfeatures/models/sort/order-for-sort.tsvia thesortsearch param - Better Auth adapter: Uses
@better-auth/drizzle-adapter/relations-v2with experimental joins (lib/auth.tsruntime,lib/auth.cli.config.tsfor generate);provider: "sqlite"; mounted on ElysiaJS at/api/[[...slugs]]/route.tswithbasePath/api/auth; OpenAPI viabetter-auth-openapi.ts
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
cacheTagutilities - Error handling with
error.tsxerror boundaries for failed queries (categories, results, and category pages with built-inreset()retry functionality) - Loading states with
loading.tsxfor results and category pages
The application uses Next.js Cache Components with granular cache tags for efficient invalidation:
- Models: Cached with
models,model-{slug}, andmodels-category-{slug}tags - Categories: Cached at component level with
categoriestag andcacheLife("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()function usessearchModels()which handles search (with optional query), category filtering, sort order, and listing. The function uses helper functionsgetModelsListandgetModelsCountwhich support optional search and category parameters; sort maps throughorderForSort - Like Status:
like-status.tsqueries use"use cache: private"for user-specific like status (cached on device) - Model Lists:
get-models.tsaddshasLikedper model after a single batched like query for the page - Invalidation:
invalidateAllModels()inutils/cache-invalidation.ts(updateTag("models")) β simple broad invalidation on like/unlike so counts andsort=popularstay correct; longer-term content/likes split documented indocs/MODEL_CACHE_SPLIT.md - Optimistic Updates: Heart button uses
useOptimisticfor immediate UI feedback with server state synchronization via form actions
- Tokens & utilities: Panda CSS 2 semantic tokens and preset utilities (
panda.config.ts,@pandacss/preset-base,@pandacss/preset-panda,@pandacss/preset-typography); orange accent and shared patterns (e.g.,navLink) live in config;treeshakeDesignSystemenabled - Typography: Albert Sans + Montserrat Alternates via
next/fontin root layout; heading font applied in PandaglobalCss - Layout & spacing: Panda
css()/ layout patterns (e.g.,gridfor model grids insrc/app/styles.ts) - View transitions: Colocated with components via React
ViewTransition+ Panda view-transition helpers (global VT CSS removed fromindex.css) - Responsive: Mobile-first breakpoints via Panda conditions and component styles
features/models/components/model-card- Individual model display cardfeatures/models/components/model-card-skeleton- Loading skeleton for model cardsfeatures/models/components/model-detail- Detailed model view pagefeatures/models/back-link/model-back-link- Server back link restoring allowlisted listingfrom(runtimeprefetch={true};arrowRecipeleft chevron micro-interactions)features/models/back-link/from-search-params-modelDetailHref/resolveBackHrefwith slugify-stable slug checksfeatures/models/components/models-grid- Grid layout for model cards (embedsfromon detail links)features/models/components/models-grid-header- Listing header: search input, query-aware title, sort controlsfeatures/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 subheadingfeatures/models/components/models-sort-controls- ClientfieldsetofSortOptionradios for A-Z / Popular / Recent (useSortQuery)features/models/components/models-sort-controls-skeleton- Loading skeleton for sort controlsfeatures/models/components/models-view- Shared server shell: header outsideSuspense, async inner awaitsgetModels; pagination usesPaginationOffsetTransitionfeatures/models/sort/components/sort-option- Single sort radio + label pillfeatures/models/sort/hooks/use-sort-query- nuqs hook forsortwith pending statefeatures/models/sort/order-for-sort- Maps sort brand to DrizzleorderByclausesfeatures/pagination/components/pagination- Reusable pagination with nuqs integration and View Transition supportfeatures/pagination/components/pagination-button- Page control button (groupfor arrow micro-interactions)features/pagination/components/pagination-limit-control- Per-page limit via customizable<select>(appearance: base-select,selectedcontent, picker transitions)features/pagination/components/pagination-offset-transition- Directional View Transition wrapper; responsive--slide-distancefeatures/pagination/components/pagination-page-control- Prev/next page buttons witharia-labelandarrowRecipechevronsfeatures/pagination/components/pagination-summary- Result count / range summaryfeatures/pagination/hooks/use-pagination-query- nuqs + View Transition hook for page/limit changesfeatures/pagination/listing-canonical- Canonical path serializer for listing SEO (query,page,limit,sort)features/pagination/listing-path- Allowlisted listingRoutevalidation (/3d-models, category listings)features/models/likes/components/heart-button-client- Client component withuseHeartLikehook, optimistic like/count state, View Transition types for count changesfeatures/models/likes/components/heart-icon- Heart glyph styled fromHeartVisualState("liked" | "unliked" | "pending")features/models/likes/components/likes-count-transition- Wraps like count withViewTransitionupdate names for increase/decreasefeatures/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 buttonfeatures/models/likes/hooks/use-heart-like- Client hook for toggle action, optimistic state, and singlevisualStatecomponents/search-input/search-input- Model search with nuqs URL state (mounted inModelsGridHeader); Enter flushes current input value;search-input-transitionfor view transitionsfeatures/categories/components/categories-nav- Category filtering sidebar (server component)features/categories/components/categories-block-transition- View transition wrapper for category listing blocksapp/3d-models/@categories/error.tsx- Error boundary for categories with built-in retry functionalityapp/3d-models/@results/error.tsx- Error boundary for search results with retry and error guidanceapp/3d-models/@results/loading.tsx- Loading state for search resultsapp/3d-models/categories/[categoryName]/error.tsx- Error boundary for category pages with retry and error guidanceapp/3d-models/categories/[categoryName]/loading.tsx- Loading state for category pagesapp/3d-models/[slug]/error.tsx- Error boundary for model detail pages with retry and error guidance
app/@navbar/default- Parallel route delegating to sharedNavbarapp/@navbar/error.tsx- Error boundary for navbar with retry functionalityapp/@footer/default- Footer parallel route with copyrightcomponents/navbar/navbar- Thin orchestrator:NavbarLogo,OfflineBanner,DesktopNav,MobileMenuButton,MobileNavPopover(smbreakpoint)components/navbar/mobile-nav-link- Mobile popover row; closes ancestor popover on navigatecomponents/offline-indicator-OfflineBannerusing Next.jsuseOffline, with persistent reserved navbar placement and container-responsive detail (hidden when online)components/nav-link/nav-link-NavLinkwithSuspensefallback, active state (includesorendsWith), border position (bottomorleft);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 forNavLinkSuspense fallbackcomponents/nav-link/nav-link-list-item-li+NavLinkwrappercomponents/nuqs/nuqs-adapter-boundary-Suspense+NuqsAdapterfor the3d-modelslayoutcomponents/top-link- Top-of-page control used in layoutsfeatures/auth/components/auth-buttons- Sign-out control wrapping avatar (authenticated navbar slot)features/auth/components/auth-buttons-skeleton- Navbar auth slot loading statefeatures/auth/components/sign-in-nav-link- Icon-only sign-in link for unauthenticated navbar slotfeatures/auth/components/auth-card- Card shell for sign-in/sign-up pagesfeatures/auth/components/auth-footer-link- Footer link between auth screensfeatures/auth/components/avatar- Avatar image with fallback
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 densitiescomponents/button-recipe- Button CVA recipe (brandOutlinefor idle sort; neutraloutlinefor GitHub sign-in)components/form/input- Text input with consistent field stylingcomponents/form/label- Accessible labels for form fieldscomponents/form/form-field- Shared labeled field shell with field errors and shared ViewTransition names for auth formscomponents/form/submit-button- Submit control wired for pending statecomponents/form/reset-button- Reset control for formscomponents/form/field-errors- Field-level error display component with ViewTransition supportcomponents/form/form-error- Form-level error display component with ViewTransition supportcomponents/not-found/unsuccessful-state- Unified component for not-found and error states with conditional styling based onisErrorpropcomponents/not-found/unsuccessful-state-list-item- List item component for unsuccessful state suggestionscomponents/pill- Small label componentcomponents/scroll-progress- Top-of-page reading progress indicator (client)components/skeleton- Shared shimmer skeleton (color/highlightColorβ CSS vars; layout viaclassName)components/suspend- Suspense helper componentcomponents/generic-component- Generic wrapper for collections
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-client- Better Auth client instance for client-side usagelib/slugify- SharedMODEL_SLUGIFY_OPTIONS/SlugifyOptionsfor seed +isModelSlugutils/with-abort- Combine AbortSignals and race promises against abortfeatures/auth/actions- Sign-in, sign-up, and sign-out server actions with Valibot validationfeatures/auth/components/has-auth- Renderschildren(auth)withUserAuthState;HasAuthSuspensewraps inSuspendfeatures/auth/constants- Validation constants (password length, email length, name length limits)features/auth/queries/get-user- User query withReact.cache()(returnsUserAuthStatefrom session)features/auth/components/sign-in-button- GitHub OAuth sign-in buttonutils/to-action-state- Action state helpers (to-action-state.ts,types.ts,form-data-to-safe-payload.tsfor allowlisted FormData β client)components/form/field-errors- Reusable field error component used in auth formscomponents/form/form-error- Reusable form-level error component used in auth forms
- Biome / Ultracite: Linting and formatting (see
biome.jsonandAGENTS.md) - React Doctor: React/Next.js diagnostics on pull requests and
mainpushes (.github/workflows/react-doctor.yml); pre-commit via lint-staged (bun run type,bun run react-doctor:staged); full local run withbun run react-doctor - Cursor hooks:
.cursor/hooks.jsonβ fastafterFileEdit(Ultracite with--skip=correctness/noUnusedImports,test:affected); heavierstop(full fix,fallow audit, full test) - TypeScript 7: Static type checking via
tsc(bun run type); Next usesexperimental.useTypeScriptCli
prepare(automatic onbun install) β Pandastyled-system/build and Husky setupbun run dev- Start development server (Turbopack; Node runtime β see Next.js runtime note above)bun run dev:inspect- Start development server with Node.js inspector (bun --bun)bun run next:upgrade- Upgrade Next.js to latest versionbun run next:analyze- Analyze Next.js bundle (experimental-analyze)bun run build- Build for production (Node runtime viavarlock run -- next build)bun run build:debug- Build with debug prerender information (bun --bun)bun run start- Start production server (Node runtime viabun run next start)bun run test- Run tests (Bun test runner;VARLOCK_ENV=testloads.env.testβ no Bitwarden)bun run test:affected- Run tests affected by git changes (bun test --changed --pass-with-no-tests; CursorafterFileEdit)bun run test:watch- Run tests in watch modebun run test:unit- Run unit testsbun run test:components- Run component testsbun run test:integration- Run integration testsbunfig.tomlβenv = false(Varlock owns.env*resolution);minimumReleaseAge(3 days) with Next/SWC excludes; test preload order:tests/setup/varlock-test-env.tsβvarlock/auto-loadβtests/setup/test-preload.ts(Happy DOM +server-onlystub);tests/setup/next-mocks.tsstubs Next navigation; nuqs components usewithNuqsTestingAdapterfromnuqs/adapters/testing; integration DB helpers intests/setup/db-test.tsuse Drizzle 1.0{ client }configbun run test:e2e- Run Playwright E2E testsbun run e2e:open- Open Playwright UIbun run e2e:codegen- Playwright codegen (localhost:3000)bun run type- Run Next typegen thentsctype checkingbun run typegen- Generate Next.js routes (next typegen)bun run env:typegen- Regeneratesrc/env.d.tsfrom.env.schema(Varlock)bun run auth:generate- Regenerate Better Auth Drizzle schema fromauth.cli.config.tsbun run db:generate- Generate Drizzle migrationsbun run db:migrate- Run Drizzle migrationsbun run db:push- Push schema directly to databasebun run db:studio- Open Drizzle Studiobun run db:seed- Seed database with initial databun run db:drop- Drop all tables (development reset)bun run fix- Fix linting/formatting issues with Ultracite/Biomebun run check- Check linting rules with Ultracite/Biomebun run doctor- Run Ultracite doctor diagnosticsbun run ultracite:upgrade- Upgrade Ultracite configurationbun 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)
The project follows a consistent coding style with:
- ES modules (import/export syntax)
- TypeScript for type safety
- Panda CSS for styling (
cssrecipes, semantic tokens) - Feature-based organization
- Component-specific type definitions
- Proper error handling and logging
- Connect your repository to Vercel
- Set environment variables in Vercel dashboard
- Deploy automatically on push to main branch
- Runtime note: Bun remains the desired package manager and future Next.js runtime. Vercel builds should use the default Next.js build (
next buildon Node) untilbun --bun+ Cache Components issues are resolved;vercel.jsonno longer forcesbunVersion/bun --bun run next build. Re-enable when upstream fixes land.
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.
- Update
src/db/seed-data/models.tswith new model data (note:userIdandlikesare omitted from seed data) - Run
bun run db:seedto update the database (requires existing users in the database)
- Add the category to
src/db/categories.ts(updates the category slug enum source of truth) - Run
bun run db:generatethenbun run db:migrate(orbun run db:pushin development) - Run
bun run db:seedto update the database
- Use centralized cache invalidation in
utils/cache-invalidation.ts - Function:
invalidateAllModels()β expires every entry tagged"models"viaupdateTag("models") toggleLikecallsinvalidateAllModels()so list counts andsort=popularstay 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 withcacheTag("session")for responsive auth state - Like status uses
features/models/likes/queries/like-status.tswith"use cache: private"for user-specific cache
- Fork the repository
- Create a feature branch
- Make your changes following the feature-based architecture
- Run tests and linting
- Submit a pull request
This project is licensed under the MIT License.
For support and questions:
- Check the documentation
- Review existing issues
- Create a new issue with detailed information