Five clusters, named as signed off, taught in the order that makes each one cheapest to learn: Prepositions in Disguise the free ones 25 verbs, one rule Aimed at Someone the list you learn 9 Facing Someone the list you learn 5 Upside Down Verbs the subject swaps 6 The Five Liars where English misleads 5 pairs The organising claim is that a verb beginning with a dative preposition takes the dative, so 25 of the 45 dative verbs cost one rule rather than 25 memorisations. The UI makes that visible instead of stating it: the prefix is printed in the dative colour wherever a verb appears, in the Lernen panel, in the rule modal and in Charts. zuhören, beitreten, widersprechen. The rule has a hole, and the lesson now says so. As a preposition, wider governs the ACCUSATIVE (wider Erwarten), so widersprechen and widerstehen being dative is not something the rule predicts. They carry a ruleExempt flag, are marked ✱ everywhere they appear, and a test fails if a verb lands in the prefix cluster whose prefix is not in DATIVE_PREFIXES without that flag — otherwise the first lesson would be teaching something false. Coverage: the earlier plan parked 19 prefix verbs in Charts as a reference list. That was wrong — a list you read is not a verb you can produce — so there are now two "Der Rest der Familie" lessons that drill them, and Daily Practice draws from all 68 verbs. Lesson one is six exemplars covering six different prefixes; nothing is taught twice and nothing is left untested, which a test asserts by set equality. Verbs on/off, defaulting to OFF. With it off every case in the app comes from a preposition, so a question never asks two unknowns at once, and the Verbs tab is hidden. On, the two tabs whose case source is a free choice go to roughly half and half. Only those two tabs change, and it is worth being precise about why: Präpositionen, Artikel, Adjektive and Komparativ lessons are named after the preposition group or the ending table they drill, so there is no verb-governed variant of "Dativ: mit, von, aus, bei" to mix in. The sign-off board claimed ~50% for every tab; that was my overstatement, not a decision, and the switch is implemented where it means something. Pronouns needed new code for this, as flagged. Every pronoun question was "Ich helfe ___", which leans on knowing helfen is dative, so with the switch off that tab had nothing to generate. PRON_PREP_FRAMES adds nine preposition-governed shapes (four accusative, five dative) — Er wartet auf mich, Er wohnt bei mir — and a test checks each frame's preposition really governs the case it claims. Pronoun questions still carry no noun on either setting, so the guard that fixed the blank screen stays real. Data: 29 new verbs. 25 prefix verbs across eight dative prefixes, ähneln, gehorchen and dienen for Facing Someone, gelingen and leidtun for the flippers (leidtun needed a `tail` field, since its clause closes with a fixed word). 45 dative verbs in total, which is where the "about 50" figure comes from once prefix verbs are counted individually rather than folded into a rule. Four things the corpus diff caught before they shipped: "I obey the child", "I serve the child", "I succeed the girl", and — the real one — "The doctor feels sorry for the doctor", because leidtun had people on both sides and Arzt/Ärztin are two German nouns with one English word. A collision scan over every verb line is now clean. The old test asserting the dative list "matches the reference" is gone; it matched my own subset, not a reference, and it now checks the cluster structure the lessons rely on and says in a comment why no canonical list exists to check against. Two things the browser pass caught that tests could not. The Lernen panel had widersprechen among the six exemplars with its prefix coloured like the rest — presenting the one verb the rule does not predict as proof of the rule, on the same screen as the note saying it is an exception. It is now ausweichen, and a test fails if a ruleExempt verb is ever used as an exemplar. And the prefix colour means "this prefix is a preposition governing THIS case", so wider- now prints in the accusative colour rather than the dative one: it really is a preposition, it just governs the other case, which is the whole reason those two verbs are exceptions. 219 tests pass. Every level in every tab renders and answers on both switch settings — 57 rows with verbs on, 45 with verbs off, no page errors — driven by clicking through the real UI rather than calling the generator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Uza6mhTpAvSwMQuFc4Cz8
Deklinationstrainer
A daily-practice German declension trainer. Self-hosted so your progress actually saves.
Deploy to Coolify
One-time setup
-
Push these files to a git repo (GitHub / Gitea / whatever Coolify is connected to).
-
In Coolify, create a new Application:
- Source: your git repo
- Build pack: Nixpacks (auto-detects Vite) or Static
- Build command:
npm run build - Publish directory:
dist - Port: not needed for static output (Coolify serves it directly)
-
Add a domain (e.g.
deklination.yourdomain.com). Coolify provisions HTTPS via Caddy/Traefik automatically. -
Deploy. Visit the URL on any device.
Local dev
npm install
npm run dev # http://localhost:5173
npm run build # outputs to ./dist
On iPhone
Open the URL in Safari, tap Share → Add to Home Screen. It opens fullscreen like a native app, and progress saves in localStorage across reboots, sessions, and app closes.
Storage
The app tries window.storage first (Claude artifact environment), then falls back to localStorage. When self-hosted, only localStorage runs, which iOS Safari persists reliably.
To wipe progress: open browser devtools → Application → Local Storage → delete the gd-v10 key. Or on iPhone: Settings → Safari → Advanced → Website Data → search your domain → delete.
Stripe checkout
Beug uses public Stripe Checkout or Payment Link URLs, supplied through environment variables. Do not put Stripe secret keys in this Vite app.
Payments are behind a launch-mode flag:
VITE_PAYMENTS_ENABLED=false
When VITE_PAYMENTS_ENABLED is absent or false, the app runs in Reddit/free mode: all lessons are usable, paid rows are not blurred, and the paywall/pricing screen is hidden. The Stripe URLs can stay configured in the environment so billing can be switched back on later with one env change.
To turn payments back on, set:
VITE_PAYMENTS_ENABLED=true
Create one product in Stripe, for example Beug Premium, with three prices:
- Weekly subscription:
$2.99every week - Yearly subscription:
$9.99every year - Lifetime access:
$19.99one-time payment
Create a Payment Link or Checkout link for each price. Set the success redirect URL to:
https://beug.george1.dev/?checkout=success
Set the cancel URL to:
https://beug.george1.dev/
Then add these public URLs in the deployment environment:
VITE_STRIPE_YEARLY_URL=https://buy.stripe.com/...
VITE_STRIPE_LIFETIME_URL=https://buy.stripe.com/...
VITE_STRIPE_WEEKLY_URL=https://buy.stripe.com/...
The current v1 unlock is intentionally simple: after Stripe redirects back with ?checkout=success, Beug stores the unlock in this browser's localStorage. This avoids accounts for the tiny-app version, but it is a soft paywall. A stricter version needs a small backend, Stripe webhooks, and account or email-based entitlement lookup.
Future prompt to re-enable payments
Turn payments back on for Beug. Set VITE_PAYMENTS_ENABLED=true in Coolify, keep the three existing Stripe Payment Link env vars, redeploy the app, and verify that unpaid users see the soft paid lesson blur, that charts remain visible, and that tapping a paid lesson opens the pricing screen with Weekly, Yearly, and Lifetime plans.