- TypeScript 98.7%
- JavaScript 0.8%
- CSS 0.2%
- Shell 0.2%
|
|
||
|---|---|---|
| .github/workflows | ||
| docker/db | ||
| docs | ||
| prisma | ||
| public/brand | ||
| scripts | ||
| skills/saatgut-mcp-agent | ||
| src | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .env.portainer.example | ||
| .gitignore | ||
| docker-compose.portainer.yml | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint.config.mjs | ||
| next-env.d.ts | ||
| next.config.ts | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.ts | ||
| postcss.config.mjs | ||
| README.md | ||
| reference.md | ||
| renovate.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
Saatgut 🌱
Saatgut is a self-hosted seed-bank and cultivation journal web app for running a practical home-growing workflow: catalog varieties, track seed batches, define frost-date-based growing profiles, derive a 14-day calendar, record planting activity, and manage seed quality signals such as germination tests and stock corrections.
📘 End-User Guide
German-first end-user documentation with validated screenshots is available in docs/benutzerhandbuch.md.
🎨 Visual Identity
The repository-level visual system is documented in:
✨ What Is Shipped
The current implementation includes:
- cookie-based registration, login, password change, invites, and session handling
- authenticated app shell with responsive navigation
- species and variety management, including tags and synonyms
- seed batch tracking with storage metadata, warning display, germination tests, and stock transaction history
- growing profiles with active-profile handling and persisted phenology state
- cultivation rules for frost-relative planning
- 14-day calendar output from the backend planning engine
- planting event capture with stock deduction
- journal logging plus operational/API surfaces for timeline, tasks, exports, backups, API tokens, MCP, and OpenAPI output
The core web UI is focused on seed-bank and planning workflows. Some operational surfaces are currently API-first rather than fully represented in the main page UI.
🧰 Stack
- Next.js 15 App Router
- TypeScript
- Prisma
- PostgreSQL 16
- Tailwind CSS v4
- Vitest
- Playwright
- Docker Compose
🗂️ Project Layout
src/app/ App Router pages and API routes
src/components/ Main client-side UI
src/lib/client/ Frontend API client and shared types
src/lib/server/ Domain services, serializers, validation, operations
src/lib/auth/ Session, password, invite, and API token helpers
prisma/ Schema and migrations
docker/ Database image and entrypoint helpers
scripts/ Operational scripts such as PostgreSQL backup
tests/ Vitest and Playwright coverage
🚀 Local Development
- Copy
.env.exampleto.env.local. - Install dependencies and generate the Prisma client:
npm install
npm run setup
- Start PostgreSQL:
docker compose up db -d
- Apply migrations:
npm run db:deploy
- Start the app:
npm run dev
Open http://localhost:3000.
🐳 Docker Compose
Run the app and database together with:
docker compose up --build
Notes:
- Compose derives the app
DATABASE_URLfromPOSTGRES_DB,POSTGRES_USER, andPOSTGRES_PASSWORD. - The bundled PostgreSQL image reconciles
POSTGRES_PASSWORDonto the persisted role during startup, which helps avoid stale-password issues on reused volumes. - The app container now receives the same runtime knobs the shipped app expects for passkeys, media storage, API limits, and MCP.
- Uploaded images are persisted in the named
media_datavolume at/app/var/media. - Container startup runs
prisma migrate deploy, so the app expects the shipped migration set inprisma/migrations/through0008_variety_companions. - This setup is intended to be self-hosting friendly and Portainer-friendly, but the README does not claim production hardening beyond the shipped health checks and container wiring.
Runtime Contract
The current deployment assets assume:
AUTH_SECRETis set to a strong random value with at least 16 characters.APP_URLis the public origin of the app and must match the URL users actually visit.WEBAUTHN_RP_IDis the bare hostname for passkeys, andWEBAUTHN_ALLOWED_ORIGINSmust include the exact browser origin.MEDIA_STORAGE_DIRpoints at writable filesystem storage; the compose variants mount/app/var/mediafor this purpose.MCP_ALLOWED_ORIGINScontrols browser-origin access to/api/v1/mcp; simple server-to-server requests without anOriginheader are unaffected.API_RATE_LIMIT_PER_MINUTEandAPI_TOKEN_DEFAULT_RATE_LIMIT_PER_MINUTEshape the in-app throttling used by REST and MCP routes.POSTGRES_DB,POSTGRES_USER, andPOSTGRES_PASSWORDremain the authoritative inputs for the containerizedDATABASE_URL.
📦 GHCR Images
GitHub Actions now publishes container images to GHCR after the verification job passes on main and version tags.
Published image names:
ghcr.io/beisel-it/saatgut:latestghcr.io/beisel-it/saatgut:mainghcr.io/beisel-it/saatgut-db:latestghcr.io/beisel-it/saatgut-db:main
The workflow also publishes immutable SHA and Git tag variants.
🧭 Portainer Deployment
For a registry-based Portainer stack, use docker-compose.portainer.yml. Start from .env.portainer.example when creating the stack environment.
Typical flow:
- Create a stack in Portainer from the repository or upload the compose file.
- Use
docker-compose.portainer.yml. - Populate the stack environment from
.env.portainer.example. - Set at least:
AUTH_SECRETAPP_URLWEBAUTHN_RP_IDWEBAUTHN_ALLOWED_ORIGINSPOSTGRES_DBPOSTGRES_USERPOSTGRES_PASSWORD
- Deploy the stack.
Notes:
- The Portainer variant pulls prebuilt GHCR images instead of building locally.
- The custom database image keeps the shipped password-reconciliation behavior for reused volumes.
- The Portainer variant also mounts a named
media_datavolume so uploads and variety images survive restarts. - If you want deterministic upgrades, set
SAATGUT_APP_IMAGEandSAATGUT_DB_IMAGEto a release tag or SHA tag instead oflatest. - Passkeys will fail if
APP_URL,WEBAUTHN_RP_ID, andWEBAUTHN_ALLOWED_ORIGINSdo not reflect the public hostname exactly. - Browser-based MCP clients should keep
MCP_ALLOWED_ORIGINSaligned with the same public origin.
✅ Verification
Run the baseline checks with:
npm run lint
npm run typecheck
npm run test
npm run build
Browser-level coverage is available via:
npm run test:e2e
🔌 API Surface
Selected shipped endpoints:
/api/health/api/v1/auth/*/api/v1/species/api/v1/varieties/api/v1/seed-batches/api/v1/seed-batches/:id/germination-tests/api/v1/seed-batches/:id/transactions/api/v1/profiles/api/v1/profiles/:id/phenology/api/v1/cultivation-rules/api/v1/calendar/api/v1/plantings/api/v1/journal/api/v1/tasks/api/v1/timeline/api/v1/exports/workspace/api/v1/backups/summary/api/v1/admin/api-tokens/api/v1/openapi.json/api/v1/mcp
🛠️ Operations
Create a PostgreSQL dump with:
npm run backup:db
Backups are written to backups/.
⚙️ Environment Notes
Important variables in .env.example:
DATABASE_URL: direct local development database connectionAUTH_SECRET: session and auth signing secretAPP_URL: app origin for local runtime behaviorWEBAUTHN_RP_NAME,WEBAUTHN_RP_ID,WEBAUTHN_ALLOWED_ORIGINS: passkey/WebAuthn runtime contractAPI_RATE_LIMIT_PER_MINUTE: baseline per-minute rate limitAPI_TOKEN_DEFAULT_RATE_LIMIT_PER_MINUTE: default rate limit for issued API tokensMEDIA_STORAGE_DIR,MEDIA_MAX_UPLOAD_BYTES: local upload storage path and upload capMCP_ALLOWED_ORIGINS: allowed browser origins for the MCP HTTP endpointPOSTGRES_*andAPP_PORT: Compose-oriented runtime settings