Two follow-ups to the apply-shape fix: opening_hours: previous converter only kept datum_von's weekday, dropping the rest of multi-day ranges. A Sat-Sun event ended up with only Samstag saved. Now iterates [datum_von, datum_bis] inclusive, projects each date to its German weekday, and dedupes by (weekday, open, close). Different hours on the same weekday still produce separate rows so the admin sees the conflict. Capped at 7 distinct weekdays — the form's expressive limit. website validator: the LLM was emitting aggregator/listing URLs (e.g. suendenfrei.tv detail pages) as the official event website because that's where it found grounding info. The prompt already forbade this, but the model ignores soft rules. Add a hard validator-level rejection for known aggregator domains: suendenfrei.tv, mittelalterkalender.info, marktkalendarium.de, festival-alarm.com, mittelaltermarkt.online. These suggestions now land in the "rejected" bucket with a clear reason. image_url / logo_url are unaffected (those legitimately come from any host). Prompt: enumerate the same aggregator domains explicitly under both the "primary source" fallback list and the "website forbidden" rule, so the model has a concrete blacklist instead of a category description. Existing markets with already-saved aggregator websites need manual clearing — the validator only kicks in on new applies. Tests: 4 new opening-hours subtests (range expansion, weekday dedupe, cross-entry dedupe, hours-conflict-kept-separate) and 4 validator subtests (three aggregator domains rejected as website; aggregator host as image_url still ok).
Marktvogt
The central portal for medieval markets ("Mittelaltermaerkte") in the DACH region.
Marktvogt connects visitors, merchants, artists, camp groups, and organizers on one platform — replacing the patchwork of Facebook groups, phone calls, mailing lists, and scattered websites that the scene currently runs on.
The name comes from the historical Marktvogt — the keeper of order at medieval markets. Same job, different century.
The Problem
The medieval-market scene in the DACH region is large (hundreds of markets per year) but digitally fragmented:
- Finding markets: Facebook groups,
mittelalterkalender.info, private sites - Bookings (merchants, artists, camps): phone, e-mail, Facebook Messenger
- Camp groups: organize almost exclusively on Facebook
- Tickets: usually box-office-only or one-off organizer sites
There is no single portal that brings all participants together. Marktvogt is that portal.
The Solution
A two-sided marketplace:
- Supply side — organizers list markets, open plots, programs, tickets
- Demand side — visitors discover markets and buy tickets; merchants, artists, and camp groups apply for spots
Network effects: organizers attract applicants, applicants make the platform indispensable for organizers, and visitors follow the content.
Roles
Six roles, with the Veranstalter (organizer) as the central actor:
| Role | Description |
|---|---|
| Gast | Anonymous visitor — browse markets, no account |
| User | Registered visitor — tickets, favorites, profile |
| Haendler | Merchant group applying for plots |
| Kuenstler | Artist group applying for performance slots |
| Lager | Reenactment camp group applying for camp space |
| Veranstalter | Organizer — creates markets, reviews applications, sells tickets |
All applicants (Haendler / Kuenstler / Lager) operate as groups. A solo merchant is just a one-person group. Mitarbeiter (staff) is a sub-role under Veranstalter with granular permissions.
Architecture
Monorepo. Components are plain directories, not git submodules.
| Layer | Stack |
|---|---|
| Backend | Go REST API + WebSocket; PostgreSQL (PostGIS), Redis, S3 |
| Web | SvelteKit + Tailwind 4, SSR for SEO, runs on Bun |
| Mobile | Flutter (Android + iOS) |
| Auth | Custom (Go), e-mail+password / magic link / OAuth / 2FA |
| Payments | Stripe Connect |
| LLM | Google Gemini (gemini-2.5-flash-lite) for enrichment |
| CI/CD | Woodpecker (ci.somegit.dev) — .gitlab-ci.yml retained as fallback |
| Hosting | Kubernetes (itsh.dev), Helm chart at helm/marktvogt/ |
| Monitoring | Prometheus, Loki, Grafana, Sentry |
Repo Layout
backend/ Go REST API + WebSocket chat
web/ SvelteKit frontend (SSR, Bun runtime)
app/ Flutter mobile app
helm/ Monolithic Helm chart (backend + web + Postgres + Dragonfly)
planning/ Vision, roadmap, MVP scope, feature specs (German, ASCII-only)
scripts/ Out-of-band tooling (e.g. K8s secret sync)
.woodpecker/ CI pipelines for somegit.dev
The admin dashboard lives in its own repo and will be added later.
Getting Started
The repo is driven by a Justfile. Run just for the full list.
# One-time setup: pre-commit hooks, web deps, golangci-lint
just hooks-install
# Backend
just backend-dev # Postgres + Valkey via docker compose
just backend-run # go run ./cmd/api
just backend-test
just backend-lint
# Web
just web-dev # SvelteKit dev server
just web-check # svelte-check (types)
just web-lint
# Everything
just lint
just test
just fmt
For component-specific details, see backend/README.md, web/README.md, and
app/README.md.
Deployment
Single Helm release marktvogt in namespace tenant-2, deployed from
helm/marktvogt/ (monolithic chart covering backend, web, Postgres, and
Dragonfly). CI runs:
helm upgrade marktvogt --reuse-values \
--set-string backend.image.tag=<sha> \
--set-string web.image.tag=<sha>
--set-string is mandatory — all-digit short SHAs get float-coerced into
scientific notation otherwise, which then fails K8s label validation.
K8s Secrets are pre-created out-of-band by scripts/k8s-secrets-sync.sh
reading from .env.helm (gitignored). CI never touches secret values.
The container registry (registry.itsh.dev/vikingowl/marktvogt.de/{backend,web})
is Zot-backed and requires attestations on every pushed image. The
Woodpecker pipelines use woodpeckerci/plugin-docker-buildx, which handles
this by default.
Status
Active development as of 2026-04-28. backend/, web/, and app/ all contain
working code (Go API scaffolding + auth, SvelteKit pages, Flutter skeleton).
- Current phase scope:
planning/15-mvp.md - Phased feature plan:
planning/17-roadmap.md - Vision:
planning/00-vision.md
Planning docs are in German and use ASCII only (no umlauts) for cross-platform compatibility.
Conventions
- API design: REST for CRUD, WebSocket for real-time chat
- Auth: custom-built using Go libraries — no external auth provider
- Offline capability: required for QR-ticket validation and venue maps
- Commits: conventional commit messages
- Source of truth for product:
planning/— read it before adding features