Roster, tactical board, and substitution planning for a soccer coach's teams.
Sports & fitnessTypeScript · CSSGitHub @Rostr-EdTechOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Soccer team management: rosters, a drag-and-drop tactical board, match and substitution planning. Becoming Rostr Coach.
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗Try it: matchday-advt.onrender.com · Create a free workspace
Head coaches can create their own private workspace for free. Matchday is becoming Rostr Coach. The first visit after a quiet period can take up to a minute while the server wakes up.
Built by Rostr EdTech: custom software designed around your process. Source published for transparency; see LICENSE.
Status lives in TODO.md, not here. This file only covers setup/usage — see
TODO.mdfor what's built so far and what's next, andCONTEXT.mdfor the architecture and why it's built this way.
A soccer team-management platform with two sides:
/portal) to see their team(s) and any feedback a coach has
chosen to share.Each head coach who signs up gets their own independent, private workspace — their own teams, athletes, and lineups, invisible to every other coach's workspace by default. A head coach can explicitly grant a colleague assistant coach access to one or more of their specific teams for real collaboration; that grant never widens beyond the teams it names.
Forked from class_copilot — see CONTEXT.md for the full domain mapping and why
that base (not the CRM root both class_copilot and college-counseling forked from) was reused here.
Kept fully independent: its own repo, own database, no shared runtime with class_copilot or the
student-check-in app (Rostr).
| Role | Sees | Can do |
|---|---|---|
| Head Coach | Their own workspace only — teams/athletes they created, plus any team an explicit assistant-coach grant gives them | Manage their own teams & athletes, assign assistant coaches, generate athlete invite links, the tactical board, the match planner, manage their own assistant-coach accounts |
| Assistant Coach | Only the teams they're assigned to (or every team taught by the one head coach who created their account, if granted all-access — never platform-wide) | Manage rosters, build lineups, and plan matches for their assigned teams only |
| Athlete | Only their own data, via /portal | See their own team(s) and any feedback a coach chose to share |
An athlete gets portal access via a one-time invite link a coach generates (they choose their own email/password when they open it) — a coach never sets or sees an athlete's password.
The app is local-first: it runs entirely on a local SQLite database with seeded demo data.
cd matchday
npm install
# Create your local env (SQLite + a generated AUTH_SECRET):
cp .env.example .env
node -e "console.log('AUTH_SECRET=\"'+require('crypto').randomBytes(32).toString('hex')+'\"')"
# → paste that AUTH_SECRET into .env (DATABASE_URL is already file:./dev.db)
npm run db:push # create the SQLite schema
npm run db:seed # head coach + 2 assistant coaches + 2 teams + 18 demo athletes
npm run dev # http://localhost:3000
npm run build compiles the whole app; npm run db:reset wipes + reseeds (destructive — only ever
run this against your local dev database).
ChangeMe123! for all)| Role | |
|---|---|
| Head Coach | [email protected] |
| Assistant Coach | [email protected] (Varsity) / [email protected] (JV) |
| Athlete | [email protected] / [email protected] / [email protected] / [email protected] |
Several demo athletes are seeded without a portal login, to demonstrate the "athlete exists before
being invited" state. Or go to /signup to create a brand-new, separate coach workspace of your own
instead of using the seeded one.
Bulk athlete-invite emails (the "Send invite links to all" button on a team's roster page) need a
Gmail address + App Password added from /admin/settings — without one, invite links still work
fine, a coach just shares them manually.
/admin/teams/[id]) — add athletes to a team, set positions/jersey numbers/injury
status./teams/board) — pick a formation, drag athletes from the bench onto a slot,
drag one slot onto another to swap, drag a slot back to the bench to unassign. Save multiple
named lineups per team./teams/matches) — schedule a match (opponent, date, number/length of periods)./teams/matches/[id]/plan) — for each period, start from a saved lineup (or
carry over who was on at the end of the previous period), then log substitutions (player off,
player on, minute). A running total-minutes-per-athlete panel flags anyone who hasn't played yet./teams/attendance) — start a practice and every active roster athlete defaults
to Present; tap a name to cycle Present → Late → Absent. "Mark all present" resets the whole
list in one tap.Next.js 15 (App Router) · React 18 · TypeScript · Tailwind + a hand-built shadcn-style UI kit · Prisma (SQLite local / Postgres-ready) · custom JWT auth (jose + bcrypt) · Zod · lucide-react.
src/lib/access.ts (assertCanAccessAthlete/assertCanAccessTeam,
accessibleAthleteIds/accessibleTeamIds, athleteScopeWhere/teamScopeWhere/
athleteIdScopeWhere/taskScopeWhere) is called by every athlete- and team-scoped action, query,
and API route. Each head coach (OWNER) is scoped to teams they coach or hold an explicit
assistant-coach grant on, and athletes they created or who are rostered on those teams — never
"every team/athlete in the database." An assistant coach (ASSISTANT) is scoped to their assigned
teams (or their one owning head coach's teams, if granted all-access — never platform-wide).src/actions/users.ts's requireOwnedAssistant — every account-management action (role
change, deactivate, delete, password reset) can only ever target an ASSISTANT the calling head
coach personally created; it can never touch another coach's account.AUTH_SECRET (no fallback), short session TTL + a sessionVersion check so
role/deactivation changes revoke stale sessions immediately, not just on next expiry.src/lib/crypto.ts/settings.ts) when set from
/admin/settings instead of .env.src/lib/allowed-email.ts,
managed from /admin/settings) — set ALLOWED_EMAIL_DOMAIN to restrict sign-up to the program's
domain, and preload approved staff/athlete emails so /signup can't be used to create a rogue
head-coach account, and an athlete record's email can't be set to an address the program hasn't
verified. Bootstrap-friendly — an empty roster just means the domain check (if any) is the only
gate, so the first admin can still sign up before preloading anything.src/lib/rate-limit.ts, in-memory/per-process —
fine for this app's single-instance deployment), to blunt scripted password-guessing without
adding friction to normal use.src/lib/validations.ts).Run the isolation + domain tests:
node --env-file=.env --import tsx scripts/isolation-test.mts
node --env-file=.env --import tsx scripts/multi-tenant-isolation-test.mts
node --env-file=.env --import tsx scripts/feedback-test.mts
node --env-file=.env --import tsx scripts/roster-import-test.mts
node --env-file=.env --import tsx scripts/attendance-test.mts
node --env-file=.env --import tsx scripts/assistant-invite-test.mts
node --env-file=.env --import tsx scripts/athlete-login-test.mts
render.yaml at the repo root is a Render Blueprint that
provisions both the web service and its own dedicated Postgres database in one step — fully
independent of any other app's database or Postgres instance (see CONTEXT.md's Ninth pass for why a
shared-instance option was considered and rolled back):
render.yaml and
creates the matchday web service and the matchday-db Postgres database together.sync: false in the blueprint) on the web service's
Environment tab:
AUTH_SECRET — a fresh random value (node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") — do not reuse the one from your local .env.NEXTAUTH_URL — the https://your-app.onrender.com URL Render assigns (or your custom domain).ALLOWED_EMAIL_DOMAIN — optional, see src/lib/allowed-email.ts.GMAIL_SMTP_USER / GMAIL_SMTP_APP_PASSWORD — optional, for bulk invite emails; can also be set later from /admin/settings instead.prisma/schema.prisma to provider = "postgresql"
inside Render's build container (see the comment at the top of render.yaml for why this never
touches the committed file — local dev keeps using SQLite), then preDeployCommand runs
prisma db push against the new Postgres database, followed by prisma/seed-formations.ts — this
always (re-)inserts the 6 built-in tactical-board formation presets, on every deploy, whether or
not you ever run the full demo seed below. They're global reference data, not demo data, so a real
coach's workspace still gets them without fake athletes showing up anywhere.npx tsx prisma/seed.ts — or skip this for a real coach's workspace and just sign up at
/signup instead.If formation presets are missing on an already-deployed instance (this shipped after some earlier
deploys — see CONTEXT.md's Eleventh pass): open a shell on the Render service and run
npx tsx prisma/seed-formations.ts once — you don't need to wait for a redeploy, and it's safe to
re-run any time (it only upserts by name, never duplicates).
Schema changes after the first deploy: prisma db push in preDeployCommand re-applies
automatically on every deploy, but it refuses (failing that deploy step, not the running app) if a
change would drop data with real rows in it — Render's own safety net on top of the one already
built into db push. A change like that needs a human to run it deliberately (Render shell, or your
own machine pointed at DATABASE_URL), or a move to tracked prisma migrate migrations if this
grows past the "solo coach's tool" stage — there's no prisma/migrations/ folder yet, only db push.