Thank you for your interest in contributing to Norish! This guide will help you get started with development.
- Node.js 22.22.0 (see
.nvmrc) - pnpm 10.x or later
- Docker (for PostgreSQL and Redis)
- Git
git clone https://github.com/norish-recipes/norish.git
cd norishpnpm installcp .env.example .env.localEdit .env.local with your local configuration. At minimum, you need:
DATABASE_URL- PostgreSQL connection stringREDIS_URL- Redis connection stringAUTH_URL- Your local URL (e.g.,http://localhost:3000)MASTER_KEY- Generate withopenssl rand -base64 32
docker run -d --name norish-db -e POSTGRES_PASSWORD=norish -e POSTGRES_DB=norish -p 5432:5432 postgres:17-alpine
docker run -d --name norish-redis -p 6379:6379 redis:8-alpinepnpm dev| Command | Description |
|---|---|
pnpm dev |
Start development server with hot reload |
pnpm dev:mobile |
Start Expo mobile workspace |
pnpm build |
Create production build |
pnpm start |
Run production server |
pnpm test |
Run tests via Turbo (all workspaces) |
pnpm test:run |
Alias for pnpm test |
pnpm test:coverage |
Run tests with coverage report |
pnpm lint |
Check for linting errors |
pnpm format |
Check formatting with Prettier |
pnpm format:check |
Check formatting without changes |
pnpm i18n:check |
Check for missing locale keys |
pnpm db:push |
Push schema changes to database |
norish/
├── apps/ # App workspaces
│ ├── web/ # Next.js app (App Router + server entry)
│ └── mobile/ # Expo app workspace (@norish/mobile)
├── packages/ # Shared libraries
│ ├── api/ # Server API logic (routers, AI, parsing)
│ ├── auth/ # Auth helpers
│ ├── config/ # Shared config
│ ├── db/ # Drizzle schema + repositories
│ ├── i18n/ # Locale tooling and data
│ ├── queue/ # Background jobs
│ ├── shared/ # Shared utilities and contracts
│ ├── shared-react/ # Shared React hooks and contexts
│ ├── shared-server/# Shared server utilities
│ ├── trpc/ # tRPC router definitions
│ └── ui/ # UI component library
├── tooling/ # Repo tooling
│ ├── eslint/ # @norish/eslint-config
│ ├── github/ # Shared GitHub Actions composite actions
│ ├── monorepo/ # Circular dependency checks
│ ├── prettier/ # @norish/prettier-config
│ ├── tailwind/ # @norish/tailwind-config
│ └── typescript/ # @norish/tsconfig
└── docker/ # Local runtime containers
- Root
package.jsonscripts are orchestration only and delegate into owned workspaces. - Monorepo control scripts live in
tooling/monorepo/scripts/. - App-owned scripts live in
apps/<app>/scripts/. - Package-owned scripts live in
packages/<package>/scripts/.
- Shared lint, format, and TypeScript settings are published as workspace packages under
tooling/. - Workspaces should compose from these packages instead of creating root-level config files.
- Current shared packages:
@norish/eslint-config(tooling/eslint) withbase,react, andnextjsexports@norish/prettier-config(tooling/prettier)@norish/tsconfig(tooling/typescript) withbase.jsonandcompiled-package.json@norish/tailwind-config(tooling/tailwind) withthemeandpostcss-configexports
- Create or update a workspace package under
tooling/with apackage.jsonand explicitexports. - Add the package to
pnpm-workspace.yamland consume it viaworkspace:*from each owning workspace. - Wire each workspace through local
package.jsonscripts (for examplelint,format,typecheck) and local config files that import shared config exports. - Run
pnpm run deps:cyclesand relevantturbo runchecks before opening a PR.
Always use the @/ path alias for imports:
// Good
import { useRecipesContext } from "@/context/recipes-context";
// Bad
import { useRecipesContext } from "../../../context/recipes-context";Never suppress TypeScript errors:
// Never use these
as any
@ts-ignore
@ts-expect-errorUse Pino logger instead of console.log:
// Server-side
import { createLogger } from "@/server/logger";
const log = createLogger("my-module");
log.info("Something happened");
// Client-side
import { createClientLogger } from "@/lib/logger";
const log = createClientLogger("MyComponent");Always use the repository pattern:
// Good - use repository
import { getRecipeById } from "@/server/db/repositories/recipes";
const recipe = await getRecipeById(id);
// Bad - direct db access in routers
const recipe = await db.select().from(recipes).where(eq(recipes.id, id));- Hooks:
use-{domain}-{type}.ts(e.g.,use-recipes-query.ts) - Components: PascalCase (e.g.,
RecipeCard.tsx) - Files: kebab-case (e.g.,
recipe-card.tsx)
git checkout -b feature/your-feature-name
# or
git checkout -b fix/your-bug-fix- Write clear, focused commits
- Follow the code style guidelines
- Add tests for new functionality
pnpm lint
pnpm test:run
pnpm i18n:check
pnpm build- Follow the PR template
- Link the PR to an issue (
Fixes #...in the PR body) - PRs without a linked issue will be closed, except translation-only PRs
- Ensure CI checks pass
Tests are colocated in workspace __tests__/ directories (e.g., apps/web/__tests__/..., packages/shared/__tests__/...). We use Vitest with React Testing Library.
# Run all tests
pnpm test:run
# Run tests for a specific workspace
pnpm --filter @norish/web run test
# Run a specific test file (from within the workspace directory)
cd apps/web && pnpm exec vitest run __tests__/hooks/recipes/use-recipes-query.test.tsNorish uses a configurable locale system. Locales are defined in packages/config/src/server-config-loader.ts and can be enabled/disabled at runtime via the Admin UI or environment variables.
Edit packages/config/src/server-config-loader.ts to add the new locale:
export const DEFAULT_LOCALE_CONFIG: I18nLocaleConfig = {
defaultLocale: "en",
locales: {
en: { name: "English", enabled: true },
nl: { name: "Nederlands", enabled: true },
"de-formal": { name: "Deutsch (Sie)", enabled: true },
"de-informal": { name: "Deutsch (Du)", enabled: true },
"your-locale": { name: "Your Language", enabled: true },
},
};This is the single source of truth for all locale configuration.
Create a new folder packages/i18n/src/messages/{your-locale}/ with the following files:
common.json- Common UI stringsrecipes.json- Recipe-related stringsgroceries.json- Grocery list stringscalendar.json- Calendar stringssettings.json- Settings page stringsnavbar.json- Navigation stringsauth.json- Authentication strings
Copy the structure from packages/i18n/src/messages/en/ as a starting point.
Expo Metro does not support fully dynamic JSON imports for locale bundles.
After adding a new locale folder, update packages/i18n/src/messages.ts and add static loader entries for every section (common, recipes, groceries, calendar, settings, navbar, auth) under MESSAGE_LOADERS.
If you skip this, iOS/Android bundling can fail with an "Invalid call" error from dynamic import(...).
Run the locale check to ensure all keys are present:
pnpm i18n:checkThis command uses en as the source of truth and reports:
- Missing keys: Keys that exist in
enbut not in your locale (CI will fail) - Extra keys: Keys in your locale that don't exist in
en(warning only)
The check runs automatically in CI and will block PRs with missing translations.
New locales are enabled by default when added to DEFAULT_LOCALE_CONFIG. You can also control this via:
- Admin UI: Go to Settings => Admin => General to enable/disable locales
- Environment variable: Set
ENABLED_LOCALES=en,nl,your-locale(comma-separated list)
By contributing to Norish, you agree that your contributions will be licensed under the AGPL-3.0 License.