Plan: Transitioning to Forward Deployed Engineer / AI Engineer
一份分阶段路线图,供后端工程师转向 FDE 与 AI Engineer 角色——解读头衔混乱、应用型 LLM 技术栈与面试循环。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A mobile-first vocabulary app for learning centers. Students study German or English words with spaced-repetition flashcards, each with their own private progress, and see how their classmates are doing. Teachers manage their course's words and see who is active on an admin dashboard.
/c/english): whoever opens it studies that course, and signing up puts them in its class. Switch courses from the pill in the top bar./admin/tests./lets-study-together./admin, per course: members, new sign-ups, daily / weekly / monthly active students, 30-day charts, a member table, the course's invite link, its name and note, and which lesson the class is on. Admins also see emails, reset passwords, make teachers and start new courses.German · Momente comes with all twelve Momente lessons, German → Uzbek, 653 words in all, each with IPA, an example sentence, plural or verb forms and a memory hook:
| Lektion | Words | Topics |
|---|---|---|
| 1 | 43 | hello, names, how are you, where from |
| 2 | 61 | age, family status, jobs |
| 3 | 34 | family, languages |
| 4 | 48 | home, furniture, shopping and prices |
| 5 | 59 | things, colours, asking how to say and spell words |
| 6 | 52 | the office, appointments, on the phone |
| 7 | 46 | hobbies, compliments, können |
| 8 | 47 | free time, making plans, weekdays, telling the time |
| 9 | 85 | food, café and restaurant, the working day |
| 10 | 62 | travel: airport, station, phoning |
| 11 | 38 | the weekend, errands, opening times, the doctor |
| 12 | 78 | trips, seasons and months, festivals, invitations at work |
Lektion 1, 2, 4, 6 and 8–12 follow the class's own word lists, with their Uzbek meanings checked and corrected. Words tagged extra aren't in those lists: useful words for the topic (the weekdays and months, furniture, bread and tea, the bus stop …). Austrian and Swiss variants (Hendl, Schlagobers, Perron …) are in the notes of the standard word rather than cards of their own. A word is never in two lessons.
English comes with four level decks, English → Uzbek, 1,306 words, marked More words coming soon. Every word has its Uzbek meaning, British IPA, irregular forms (go · went · gone, child · children, good · better · best), an example sentence with its Uzbek translation, and notes in Uzbek on the usual slips (I'm 19, not I have 19 years; bored vs boring; borrow vs lend). Memory hooks and notes are in Uzbek, so beginners can read them:
| Deck | Words | Topics |
|---|---|---|
| A1 | 375 | greetings, family, numbers and time, days and months, food, the home, first verbs, adjectives and colours, question words, the body, clothes, town and travel, weather, school, free time |
| A2 | 327 | travel and directions, health and feelings, personality, home life, shopping and eating out, school, jobs, technology, nature, time words, phrasal verbs |
| B1 | 304 | work and money, relationships, agreeing and arguing, environment, society and law, culture and news, education and ideas, lifestyle, sport, travel, linking words |
| B2 | 300 | analysing and arguing, change and problems, character and strong feelings, economy and law, science, health and media, essay linking words, formal verbs |
A word is never in two decks of a course. New built-in lists are added to an existing database once, on the next start (SEED_PACKS and SEED_COURSES in src/lib/seed.ts); words a teacher deleted don't come back, and words already typed in by hand aren't doubled.
Built with Next.js 16 (App Router), React 19, TypeScript and PostgreSQL (Supabase).
You need Node.js 20.9 or newer (node -v to check) and a Postgres database. A free Supabase project is the easiest (2 minutes, see Supabase below).
npm install
cp .env.example .env.local # then paste your Supabase connection string into DATABASE_URL
npm run dev
The tables are created on the first start.
Become the admin (teacher). Put your email in ADMIN_EMAILS in .env.local, then either:
NEXT_PUBLIC_GOOGLE_CLIENT_ID, see Sign in with Google), orRESEND_API_KEY it is printed in the terminal) → choose a password → log in.Then open http://localhost:3000/admin. Share the sign-up link with your students; everyone who signs up joins the class leaderboard.
Run it on your computer and open it from phones on the same Wi-Fi:
npm run dev -- -H 0.0.0.0
Find your computer's local IP (macOS: ipconfig getifaddr en0, Windows: ipconfig) and open http://<that-ip>:3000. Share → Add to Home Screen gives a full-screen app icon. To let students use it from home, deploy it (see below).
eu-central-1 is a good choice). Save the database password it asks for.npm run dev and for a normal server..env.local as DATABASE_URL, with your password in place of [YOUR-PASSWORD] (brackets included). Passwords with special characters (@ # / ? % +) work as they are; the app encodes them. Keep the password only in .env.local (ignored by git), never in .env.example.npm run dev. On the first start the app creates its tables and loads the built-in word lists.Open Table Editor in Supabase to see the data: users (profiles; password_hash is a one-way bcrypt hash, never the password), words, cards (each person's progress), password_reset_tokens and rate_limits (only hashes), and so on. The app talks to Postgres directly from the server, so you don't need Supabase's API keys or supabase-js. Row Level Security is switched on for every table with no policies, which keeps them closed to Supabase's public REST API.
Why it stays fast
words.owner_id, cards (user_id, word_id), activity.day, …).Set ANTHROPIC_API_KEY in .env.local (create a key at console.anthropic.com → API keys) and restart. A ✨ button appears on the Words tab for everyone with a profile.
AI_MODEL to use a different model.How it works: src/lib/server/ai.ts makes one streamed request with the image (or list), a system prompt describing a good flashcard, and a JSON schema the answer must follow (output_config.format). If Claude's safety checks decline a request, fallbacks: "default" lets the API retry it on its recommended fallback model. The answer is validated like any user input before the client sees it.
The German course has five tests in a Tests tab (courses without tests don't show the tab; guests see it locked). Each copies the Goethe A1 exam: instructions in German with an Uzbek translation underneath, five sections of 20 points, grades sehr gut (90+), gut (80+), befriedigend (70+), ausreichend (60+), nicht bestanden.
| Test | Lessons | Grammar |
|---|---|---|
| Test 1 | 1–3 | verb endings, sein, haben, W- and yes/no questions, verb second, mein / dein / Ihr |
| Test 2 | 4–6 | der / die / das, ein / kein, Akkusativ, plural, the time, am / um / von … bis, Sie-imperative |
| Test 3 | 7–9 | können, möchten, mögen, separable verbs, vowel-change verbs, gern / lieber, am / um / im |
| Test 4 | 10–12 | Perfekt, war / hatte, du- and ihr-imperative, müssen / dürfen, dates, nach / zum / zur, ihn |
| Abschlusstest | 1–12 | all of it, with three listening and three reading parts, 26 grammar items and five speaking tasks |
src/lib/server/tests/); the browser gets the questions only. Typed answers forgive case, spaces, a final dot and ae / oe / ue / ss for ä / ö / ü / ß. Writing and speaking left empty score 0.ANTHROPIC_API_KEY, like the word helper): one request per attempt scores writing (3 points per content point + 1 for greeting and closing) and speaking (by the Goethe rubric, from the words only, not pronunciation), with corrections, a short explanation in the student's interface language and a tip. 8 checks per hour per student, one at a time per attempt; roughly 3–6 US cents per attempt with Claude Opus 5.5 (check real spend in the Anthropic Console). Without a key, the result shows the model answers and those two sections stay unscored./admin/tests shows each student's best score per test, the average and pass rate per test, and the latest attempts with their five section scores; any score opens the whole attempt with the student's text and the AI feedback. Names only, no emails.src/lib/server/tests/ (see german-a1.ts; npm test checks every test: unique ids, valid answers, a perfect attempt scores 100) and add it to TESTS.A card at the top of the Class tab (or the link /lets-study-together) opens it.
APP_TIMEZONE) and press I want to study. Hours can change until Sunday 18:00; after that, sign-ups count for the week after./admin/study): next week's best times as a heatmap (how many are free each hour, never who; it appears once 3 students have signed up and shows only hours at least 2 share, so no one's hours stand out), sign-up and group counts, speaking clubs (pick an hour, a room or an https link; students tap I'll come), and reports: who reported whom and why. Teachers and admins can't read any chat: no page, route or query exists for it. Teachers are never put in groups.ADMIN_EMAILS to the people who run the app./api/cron/study on Sunday at 13:00 UTC (18:00 in Tashkent; vercel.json) with your . If that hasn't happened, the first visit after 18:00 makes them. Either way it happens once per course and week (the claim and all groups are written in one statement). If you change , change the hour in too.openid, email, profile), which need no Google review.http://localhost:3000, http://localhost and https://german-roan.vercel.app (plus your own domain if you add one). Authorized redirect URIs can stay empty: the button uses Google's popup and hands the app an ID token directly.….apps.googleusercontent.com) into NEXT_PUBLIC_GOOGLE_CLIENT_ID. There is no client secret to set.The server checks every Google ID token with google-auth-library (signature, audience = your client ID, issuer, expiry) before using it. The first Google sign-in creates the profile, or links the profile that already has that email (only when Google says the address is verified). Changes in Google Cloud can take a few minutes to apply.
RESEND_API_KEY.EMAIL_FROM (e.g. Karteikasten <[email protected]>). Until then, Resend's test sender [email protected] only delivers to your own Resend account's email.APP_URL if the app runs on your own domain (on Vercel it defaults to the production domain).Without RESEND_API_KEY, npm run dev prints the reset link in the terminal so you can test the whole flow. Links work once, for 45 minutes; a new request replaces older links, and using one signs the profile out on every device.
Copy .env.example to .env.local. Only DATABASE_URL is required.
| Variable | Default | What it does |
|---|---|---|
DATABASE_URL | – (required) | Postgres connection string (Supabase → Connect). |
DATABASE_CA_CERT | – | Supabase's CA certificate, to also verify the server's identity. Without it the connection is still encrypted. |
DATABASE_POOL_MAX | 5 | Database connections per server process. |
ANTHROPIC_API_KEY | – | Turns on the AI word helper and the AI examiner for mock tests. Without it the AI buttons don't appear. |
AI_MODEL | claude-opus-5-5 | The Claude model the AI helper uses. |
AI_ENABLED | auto | false hides the AI helper even with a key; true turns it on with other Anthropic credentials (e.g. ant auth login). |
ADMIN_EMAILS | – | Your email (comma-separated for several). It becomes admin once proven: via Google, or a password reset link. |
NEXT_PUBLIC_GOOGLE_CLIENT_ID | – | Google OAuth Web client ID. Turns on Continue with Google. Public; needs a rebuild to change. |
RESEND_API_KEY | – | Sends the password emails. Secret. Without it, development prints the link in the terminal. |
EMAIL_FROM | Karteikasten <[email protected]> | The sender of password emails; its domain must be verified in Resend. |
APP_URL | Vercel's production domain | The site's address for links in emails. |
APP_TIMEZONE | Asia/Tashkent | When a "day" starts for streaks and daily active users, and the hours of Let's study together. |
CRON_SECRET | – | Any long random value. Vercel sends it to /api/cron/study, the Sunday job that makes the study groups. Without it the groups are made on the first visit after Sunday 18:00. |
COOKIE_SECURE | auto | Force /. By default the session cookie is Secure whenever the site is on HTTPS. |
| Guest (no profile) | Student | Course teacher | Admin | |
|---|---|---|---|---|
| Study the flashcards, settings | ✓ (saved in this browser) | ✓ | ✓ | ✓ |
| Browse the word list | ✓ (read-only) | ✓ | ✓ | ✓ |
| Progress, streak, daily goal | goal and streak on this device | ✓ | ✓ | ✓ |
| Add, edit, import, delete your own words (only you see them) | – | ✓ | – | – |
| Class leaderboard: classmates' streaks, active words, answers; send cheers | – | ✓ | ✓ | ✓ |
| AI word helper (costs the center API credit; hourly limit) | – | ✓ | ✓ | ✓ |
| See email addresses (anyone's, or your own) | – | – | – | ✓ |
| See anyone else's cards | – | – | – | – |
| Add, edit, import, delete the shared lesson words | – | – | their course | every course |
| Take mock tests, see your own results | – | ✓ | ✓ | ✓ |
/admin/tests: everyone's mock-test results and answers | – | – | their course | every course |
| Study groups: sign up, chat in your own group, leave, block, report | – | ✓ (on the class board) | – | – |
| Read a study group's chat | – | its members only | – | – |
/admin/study: free-time heatmap (counts), speaking clubs, reports (no messages) | – | – | their course | every course |
/admin and /admin/analytics: stats, the class lesson, the course's name and note, take people off the leaderboard | – | – | their course | every course |
| Reset passwords, make teachers and admins, delete profiles, start new courses | – |
Courses. Each course is its own class: its words, members, leaderboard, cheers, teachers and current lesson. Everyone joins a course when they sign up (the one whose link they followed, else the one they picked as a guest, else German) and can join more from the course pill; their progress, own words and lesson choice in each course stay. Answers, streaks and the daily goal count all of a person's courses together. A new guest sees What are you learning? first; a course link such as /c/english skips that. An admin starts a course in /admin → New course (name, link, language studied, note for students); its words come from the importer, the AI helper or a built-in list.
Cards follow the course's language. German has the der/die/das quiz, gender colours and German voices; English has none of that, British (or American) voices, and English dates on the study page. The AI helper writes cards for the course's language, and "All decks" understands both Lektion N / Unit N and levels (A1 … C2) when a class lesson is set.
No class code (for now). Everyone who signs up is on their course's leaderboard and can use the AI helper. A course's teacher can take someone off the leaderboard in /admin (they keep studying, but aren't listed and can't send cheers).
The class lesson. In /admin, set The class is on to the lesson or level the class has reached. Studying “All decks”, everyone in that course (guests too) then gets new words from that lesson first, then the lessons before it, newest first (10, 9, 8 … or B1, A2, A1), so nobody starting today is sent back to the beginning. Later lessons wait until you move the setting on; anyone can still tap a lesson to study it early, and words already started keep their reviews. The lesson is marked class on the Study screen. Not set: new words come in book order.
Admins and teachers. Nobody becomes admin by signing up first. An address in ADMIN_EMAILS becomes admin once it's proven to be theirs: a Google sign-in (Google verified the address) or a password reset link sent to it. Admins run every course. In /admin → Manage, an admin makes a member teacher of this course (they then edit that course's lesson words in the app and open its dashboard, without seeing emails) or makes someone admin.
Email privacy. Students and course teachers see names and photos, never email addresses: not classmates', and not their own (the menu and /account show only name and photo). The server leaves emails out of everything it sends them; only admins get them.
Language. Each person picks Oʻzbekcha or English in Study settings; it is saved with their settings, so it follows them to every device, and the browser remembers it for the login page. Before anyone chooses, Uzbek phones get Uzbek. The translations live in src/lib/i18n-uz.ts, keyed by the English text; i18n.test.ts fails if any text the app shows is missing there. The built-in memory hooks are content, not interface: the German ones are in English, the English course's are in Uzbek.
Guests. / opens straight to the flashcards. Guest progress stays in the browser; guests never show up in the admin numbers or the Class tab. For the funnel on /admin/analytics the browser sends one anonymous count a day: a random id it made up (kept in localStorage as kk.visitor), the course and whether it has answered a card. No name, no IP address is stored. The course a guest picked is remembered in a kk_course cookie (only the course id), so signing up or logging in lands them in it; it is cleared once a profile has taken it over, so on a shared computer the next person keeps their own course. If that browser later signs up, the id is linked to the new profile, so the funnel can tell who tried the app first. The word list itself is public, because guests need it to study. On sign-up, the guest's progress and settings move into the new profile. A guest who logs in to a profile that already has progress is asked first (Add to my profile / No, discard it), because on a shared classroom computer that guest may have been someone else. Words the profile already studied always keep the profile's progress.
Only counts are shared on the Class tab. Nobody, the admin included, sees another person's individual cards, answers or own words.
Words. One words table holds both kinds, each in a course (course_id): owner_id is empty for a shared lesson word and holds the student's id for their own word. The server only ever returns your course's shared words plus your own words in it, and only lets the owner (or, for lesson words, that course's teachers and admins) change or delete a word. Each person can have up to 2,000 own words. Deleting a profile deletes their own words and progress with it.
Forgotten password? Forgot password? on the login page emails a link (it always answers "If that email exists, we've sent a link", so nobody can test which addresses have profiles). Google-only profiles use the same link to set a password. A teacher can also open the student in /admin → Reset password for a temporary password like tante-fest-4821; they change it under Your profile → Account and password.
| Number | Definition |
|---|---|
| Members | Everyone in the course, its teachers included |
| New this week | Joined the course in the last 7 days |
| Studied today (daily active) | Answered at least one card today |
| Opened today | Opened the app today, whether or not they studied |
| Active this week / month | Studied on at least one of the last 7 / 30 days |
| Streak | Days in a row with at least one answer (today counts once you study) |
| Active words | Words a student has started learning and not paused |
"Today" follows APP_TIMEZONE.
| Command | What it does |
|---|---|
npm run dev | Development server with hot reload |
npm run build / npm start | Production build and server |
npm test | Unit + database integration tests (Vitest). The database tests run on PGlite, an in-memory Postgres, so they need no setup. TEST_DATABASE_URL=postgres://… npm test runs them on a real Postgres instead (use an empty database you can throw away). |
npm run typecheck | Generate route types, then tsc --noEmit |
npm run lint | ESLint (Next.js + React Compiler rules) |
All of it is in src/lib/srs.ts (pure functions, tested in src/lib/srs.test.ts).
New cards go through short learning steps: 1 minute, then 10 minutes.
When you get a card right, it graduates to a 1-day interval (Easy: 4 days).
Review cards grow by the card's ease factor (EF, starting at 2.5): Good × EF, Hard × 1.2, Easy × EF × 1.3.
After each review, EF is updated with the SM-2 formula, where Again = 2, Hard = 3, Good = 4, Easy = 5:
EF' = EF + 0.1 − (5 − q)(0.08 + (5 − q) · 0.02), never below 1.3
Worked example, Hard (q = 3): 5 − 3 = 2 → 0.08 + 2 × 0.02 = 0.12 → 2 × 0.12 = 0.24 → 0.1 − 0.24 = −0.14.
Again on a review card halves the interval, drops EF by 0.32 and sends the card to a 10-minute relearning step. After 4 lapses a word is a leech.
A small ±5% seeded "fuzz" spreads words learned together over neighbouring days.
Pressing Good every time gives intervals of 1 → 4 → 10 → 24 → 60 → 144 days.
Browser
/login, /signup, /forgot-password, /reset-password, /account
── Server Actions (src/app/actions/auth.ts): zod, bcrypt, session cookie, Resend
── Google button → POST /api/auth/google (google-auth-library checks the ID token)
/ ── client app ── useStore() ── fetch /api/* (guests: localStorage for progress)
/admin ── Server Component behind <Suspense> ── Server Actions (src/app/admin/actions.ts)
src/proxy.ts /admin, /account without a session cookie → /login?next=… (fast first check only)
src/app/api/* requireUser() on every route: valid session, role, same-origin
src/lib/server/ repo.ts (all SQL) · db.ts (pool, schema, seed) · session.ts · password.ts · rate-limit.ts
google.ts · mail.ts · admins.ts · ai.ts · ai-grade.ts (AI examiner) · tests/ (mock tests with their answer keys)
study.ts (study groups: matching, chats, clubs, reports)
src/lib/ auth-schemas.ts (zod: the same rules in the browser and on the server) · tests/ (test types and scoring)
Postgres/Supabase courses · course_members · users · sessions · password_reset_tokens · rate_limits · words · cards · prefs
· activity · cheers · visitors · settings · test_attempts · study_* (sign-ups, groups, members,
messages, blocks, reports, clubs, runs)
| Endpoint | Who | Purpose |
|---|---|---|
GET /api/state | anyone | Signed in: you, your course and the list of courses, its words, your progress, settings, new cheers. Guest: { me: null, words, course } (no course yet: course: null, no words) |
POST /api/course | anyone | { id } switches course: signed in, you join it; guests: remembered in this browser |
GET /c/:course | anyone | A course's invite link: opens the app in that course (signed in: join it) |
GET /api/me | signed in | Who you are: name, photo, role, how you log in (email for admins only); 401 when signed out |
POST /api/auth/google | anyone | { credential } from the Google button → verifies it, signs in (creates or links the profile) |
POST /api/cards/import | signed in | Add progress made as a guest (never overwrites existing progress) |
GET /api/health | anyone | Is the database reachable, and if not, why (no secrets shown) |
POST /api/visit | anyone | Anonymous guest count for the funnel: { id, tried, course }, at most once a day per browser (rate limited) |
POST /api/ai/words | signed in | AI word helper: photo or typed list → suggested flashcards (nothing is saved) |
GET /api/tests | signed in | Your course's mock tests and your attempts |
GET /api/tests/:id | signed in | A test's questions, without the answers |
POST /api/tests/:id/submit | signed in | { answers, startedAt } → scored and saved (30 an hour) |
GET /api/tests/attempts/:id | signed in | One attempt with the answer key: yours, or your course's as its teacher |
POST /api/tests/attempts/:id/feedback | signed in | { lang } → the AI scores the writing and speaking of your attempt (8 an hour) |
src/lib/auth-schemas.ts); the browser runs the same schemas for instant feedback.localStorage. The database stores only its SHA-256, so a leaked database can't be used to log in. 30-day sliding expiry; a password change signs out other devices, a reset signs out all.rate_limits, keys hashed), so they hold across all Vercel instances: login 30 per IP and 10 per email per 15 minutes; sign-up 20 per network per hour; password links 10 per IP and 3 emails per address per hour; Google sign-in 30 per IP per 15 minutes./admin./admin/analytics)The teacher's analysis page, built on the metrics from the product deck. Only students count (admins testing the app would blur the numbers), and a "week" is the last 7 days, then the 7 before, and so on.
| Section | What it shows |
|---|---|
| Users | Students, new this week, active today / week / month, stickiness (average daily actives ÷ monthly actives) and guests this week |
| North Star: words mastered per week | A word counts as mastered the first time one of its review gaps reaches 21 days. It only grows when students come back over weeks and still remember, so it measures habit and real memory, not app opens. 12-week chart, change vs last week, per active learner |
| What moves it (inputs) | Active learners, study days per learner, new words started, recall rate (reviews answered Good or Easy) |
| Student journey (funnel) | Guests: Visit (opened the app without a profile) → Try (answered a card) → Join (made a profile within 7 days). New profiles: Join → Start (20+ answers on day one) → Return (studied in week two) → Habit (3+ study days in a week). Each step shows its conversion, and the biggest drop-off is highlighted. Pick 30 days, 90 days or all time |
| Health checks | Recall rate, answers per study day, cheers per learner and number of reviews, this week vs the 4-week average |
| Retention by sign-up week | Of the people who joined in a week, the share who studied in week 1, 2, 3, … after joining |
Nothing extra is tracked for students: mastery and recall come from the review history every card already keeps (each card stores its last 30 answers), and older databases are filled in from that history once on the first start of this version. Guest visits are counted from this version on.
Vercel Web Analytics (page views, visitors, countries, devices) is in the Vercel dashboard → your project → Analytics. The app loads Vercel's script on Vercel deployments only; enable Web Analytics there once.
Free option: Vercel + Supabase
DATABASE_URL (the pooler string with your password, no quotes, for the Production environment), ADMIN_EMAILS, NEXT_PUBLIC_GOOGLE_CLIENT_ID, RESEND_API_KEY, EMAIL_FROM, APP_TIMEZONE, CRON_SECRET (for the Sunday study groups) and, for the AI helper, ANTHROPIC_API_KEY (and APP_URL if you use your own domain). .env.local is never uploaded, so this step is required.NEXT_PUBLIC_GOOGLE_CLIENT_ID is built into the page.https://<your-app>/api/health. It should say "ok": true. Then Continue with Google with your ADMIN_EMAILS address, and /admin opens."This page couldn’t load … A server error occurred" when logging in or signing up means the server can't use the database. Open https://<your-app>/api/health: it names the problem (missing DATABASE_URL, wrong password, host not found, paused Supabase project, …) and what to do, without showing any secrets. Supabase pauses free projects after about a week without activity; restore it from the Supabase dashboard.
Or any Node.js server (Render, Railway, a VPS): set the same variables, then npm run build && npm start.
pg_dump "$DATABASE_URL" > backup.sql.localStorage.localStorage (kk.guest.cards, kk.guest.prefs) until it moves into a profile. Clearing the browser's site data deletes it.localStorage (kk.visitor). Clearing site data makes the browser count as a new visitor.Profiles from before email sign-in (username only) can't log in any more: they have no email. /admin lists them as "no email (old profile)"; delete them there, and have those people sign up again.
npm run build prints "Failed to find font override values for Atkinson Hyperlegible …". That is harmless: Next.js has no fallback metrics for these newer fonts yet.
Pronunciation uses the voices installed on the device. On iPhone, German and English voices are under Settings → Accessibility → Spoken Content → Voices.
Several centers on one app. Vercel's free Hobby plan is meant for non-commercial use; once paying centers rely on it, move to Vercel Pro. Supabase's free tier holds thousands of students.
npm audit --omit=dev finds 0 vulnerabilities in what the app runs. The advisories npm audit shows are in dev tools (the Vitest 3 test runner and Next.js's ESLint config). Vitest 4+ fixes its part, but npm 10's installer crashes on it; if npm -v shows 11 or newer, run npm i -D vitest@latest.
CRON_SECRETAPP_TIMEZONEvercel.jsontruefalse| – |
| – |
| ✓ |
GET /api/study |
| signed in |
| Your study week: sign-up, groups (names only), clubs; makes due groups first |
PUT/DELETE /api/study/signup | students | { slots } → your free hours for next week, or withdraw |
GET/POST /api/study/groups/:id/messages | group members | Read newer messages (?after=), or post { body }; anyone else gets 404 |
POST /api/study/groups/:id/leave · /block · /report | group members | Leave; { member } block; { member, reason } report (and block) |
POST /api/study/clubs/:id/rsvp | course members | { going } for a speaking club of your course |
GET /api/cron/study | Vercel Cron (CRON_SECRET) | Makes the due study groups, deletes expired ones |
POST /api/logout | signed in | Sign out; a plain form post, so the full page reload clears the old profile from memory |
PUT/DELETE /api/words/:id, POST /api/words | signed in | Add, edit, delete, import words in your course: its teachers the shared lesson words, everyone else their own |
PUT/DELETE /api/cards/:wordId, DELETE /api/cards | signed in | Save or reset your progress (reset: this course only) |
PUT /api/prefs | signed in | Your settings |
GET /api/class | signed in | Your course's leaderboard: classmates' streaks and counts (no emails) |
POST /api/cheers, POST /api/cheers/seen | signed in | Send a cheer (1 per classmate per day), mark yours as read |