Artisanal Futures

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, including check-user-permissions.ts (ownership checks) and coolify.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 role ADMIN or ARTISAN.
  • adminOnlyProcedure — requires role ADMIN.
  • artisanProcedure — requires role ADMIN or ARTISAN, and additionally attaches shopsAvailable to context (all shops for admins, only owned shops for artisans).
  • protectedDevelopmentProcedure — requires a session and NODE_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:

  1. tRPC procedures (src/server/api/trpc.ts) — adminOnlyProcedure, adminArtisanProcedure, and artisanProcedure gate entire endpoints by role before the handler runs.
  2. Layout guards — e.g. src/app/admin/layout.tsx redirects to sign-in if there's no session, and to /unauthorized unless the user's role is ADMIN or ARTISAN.
  3. Ownership checks (src/lib/check-user-permissions.ts) — functions like checkUserShopPermissions, checkUserProductPermissions, checkUserServicePermissions, checkUserOwnsProducts, and checkUserOwnsServices verify that a non-admin user actually owns the shop/product/service they're trying to act on (admins always pass).

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.

On this page