Architecture
How the Artisanal Futures codebase is laid out, and how auth and role enforcement work.
App Router layout
Routes live under src/app/, following Next.js 15 App Router conventions. The top-level route groups are:
(site)— the public marketplace: homepage, shop/product/service browsing, account pages, donate, legal, contact, join, tools, and the welcome/onboarding flow.admin— the admin panel (categories, shops, products, services, events, surveys, upcycling, users, invites, website provisioning, fork-import).auth— sign-in, sign-up, sign-out, and error pages for better-auth.forums— communities, subreddit-style community pages (r/), post creation, content policy, and user agreement.api— route handlers, including the better-auth catch-all, uploads, checkout (Stripe donations), and the SimplePress webhook callback.
Other top-level directories under src/:
src/components/— shared React UI components.src/server/— backend code:api/(tRPC routers),better-auth/(auth config/client/server),db.ts(Prisma client),jobs/,fork-import/.src/trpc/— tRPC client setup for React, server, and query client config.src/lib/— shared utilities, includingcheck-user-permissions.ts(ownership checks) andcoolify.ts(deployment/provisioning client).src/env.js— the environment-variable schema and validation.src/middleware.ts— Next.js middleware, runs on requests before they hit a route.
tRPC routers
tRPC 11 routers live in src/server/api/routers/ and are wired together in src/server/api/root.ts. Current routers: auth, category, contact, event, forum, forum-subreddit, invite, migration, onboarding, product, service, shops, surveys, upcycling, user, website-provision.
Shared procedure builders live in src/server/api/trpc.ts:
publicProcedure— no auth required.protectedProcedure— requires a logged-in session.adminArtisanProcedure— requires roleADMINorARTISAN.adminOnlyProcedure— requires roleADMIN.artisanProcedure— requires roleADMINorARTISAN, and additionally attachesshopsAvailableto context (all shops for admins, only owned shops for artisans).protectedDevelopmentProcedure— requires a session andNODE_ENV === "development"(dev-only debug endpoints).
Prisma directory schema
The schema is split across the prisma/ directory rather than a single file: prisma/schema.prisma plus per-model files under prisma/models/ (shops.prisma, forum.prisma, invite.prisma, categories.prisma, products-and-services.prisma, events.prisma, surveys.prisma, websites.prisma, and more).
Most Prisma CLI commands need --schema ./prisma to see the whole model set — without it, commands fail with "type is neither a built-in type, nor refers to another model." The db:* scripts in package.json already bake this flag in; prefer them over raw prisma calls.
The generated Prisma client is emitted to generated/prisma (not the default node_modules/@prisma/client location) — code imports it as from "generated/prisma" or via ~/server/db, which wraps it.
Authentication: invite-only sign-up
Auth runs on better-auth (src/server/better-auth/config.tsx), backed by the Prisma adapter. Sign-up is gated by a before hook on the /sign-up/email path: it looks up a PlatformInvite row by code, and rejects the request if the invite is missing, already used, expired, or issued for a different email address. There is no self-serve sign-up outside this invite flow — Discord, Google, and Auth0 social providers are all configured with disableImplicitSignUp: true.
The Role enum (prisma/schema.prisma) has six values: USER, ADMIN, ARTISAN, DRIVER, GUEST, MANAGER. Role is stored as an additional field on the better-auth User model.
Role enforcement layers
Role checks happen at three layers:
- tRPC procedures (
src/server/api/trpc.ts) —adminOnlyProcedure,adminArtisanProcedure, andartisanProceduregate entire endpoints by role before the handler runs. - Layout guards — e.g.
src/app/admin/layout.tsxredirects to sign-in if there's no session, and to/unauthorizedunless the user's role isADMINorARTISAN. - Ownership checks (
src/lib/check-user-permissions.ts) — functions likecheckUserShopPermissions,checkUserProductPermissions,checkUserServicePermissions,checkUserOwnsProducts, andcheckUserOwnsServicesverify that a non-admin user actually owns the shop/product/service they're trying to act on (admins always pass).
Public catalog search
Public catalog search lives in src/lib/search/catalog-search.ts (accent/case folding, tokenization, word-start matching, per-shop result interleaving). It has an offline, self-checking verification script — pnpm tsx scripts/verify-search.ts — that tests the pure helpers synthetically and replays real queries against production-export.json, with no database needed.
Product sync engine
The scheduled product-sync feature lives in src/server/lib/product-sync.ts (planning and applying sync runs) and src/server/lib/store-feed.ts (per-platform feed fetching for Shopify, WordPress, Squarespace, and SimplePress), both built on top of the SSRF-hardened src/server/lib/safe-fetch.ts. src/server/api/routers/product-sync.ts exposes the review queue over tRPC, and src/app/api/cron/sync-products/route.ts is the weekly entry point (see Deployment for the Coolify schedule). Two offline test suites exercise this in-memory, with no database: pnpm test:sync (scripts/test-product-sync.ts, 113 checks against fake data) and pnpm exec tsx scripts/test-safe-fetch.ts (91 checks on the fetch-safety helpers). scripts/check-store-feed.ts previews what a live feed would return without planning a run, and scripts/dedupe-products.ts is a dry-run-by-default tool for merging pre-existing duplicate products (it hides losers rather than deleting them, and backs up rows first).
See Database for the schema/migration details, and Setup to get this running locally.