8000 GitHub - aryansinha1908/LifeRPG · GitHub
[go: up one dir, main page]

Skip to content
aryansinha1908Public

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Life RPG

A cozy habit tracker that plays like a role-playing game.

Ordinary to-do lists have a delayed-gratification problem: reading a book or going to the gym takes months to show a result, so the list itself feels like a chore. Life RPG closes that gap. Finish something real — wash up, run 5k, ship the auth layer — and your character levels up this second: XP lands, an attribute grows, Seeds drop, and a streak stays alive.

The stats are not decorative. Everything is computed and stored server-side, so they cannot be faked from the browser.

Live app: add your deployment URL here Walkthrough video: add your video link here


Try it in 60 seconds

cp .env.example .env     # then fill in MONGODB_URI and AUTH_SECRET
npm install
npm run seed             # optional: a populated demo account
npm run dev

Open http://localhost:3000.

The seed creates a deterministic demo character — demo@liferpg.app / cozy-demo-2026 — at level 12 with 267 finished quests across 41 days, 2,713 Seeds to spend, and an 8-day streak that ends tonight. That last detail is on purpose: the stakes are visible the moment the page opens, and ticking any one quest saves the streak.

To see the product from zero instead, sign up with your own email and finish one Hard quest — that is 120 XP, which is an instant level-up.


What it does

Requirement How it works
Auth & security Auth.js (NextAuth) credentials provider, bcrypt-hashed passwords, JWT session in an httpOnly cookie. Every query is scoped by the session's user id.
Schema & CRUD Five MongoDB collections — users, characters, tasks, an append-only completion ledger, and per-day activity. Full create/read/update/delete on quests.
Non-linear levelling xpForLevel(L) = 25L² − 25L + 100. Each level costs exactly 50L more than the last: 100, 150, 250, 400, 600 …
Streaks Consecutive days with a finished quest, keyed to your local civil date. Worth up to a 1.5× multiplier.
Attributes Nine quest categories feed six stats — Strength, Intellect, Vitality, Discipline, Charisma, Creativity — on their own gentler curve.
Economy Finished quests pay Seeds. Spend them in The Little Shop on badges, desk trinkets, and themes that repaint the entire app.
Responsive & accessible Mobile-first, fully keyboard navigable, semantic landmarks, and progress announced to screen readers.

The progression engine

XP to advance from level L:

xpForLevel(L) = 25L² − 25L + 100          →  difference is 50L, always positive

Cumulative XP to reach level L, closed form with n = L − 1:

totalXpToReach(L) = 25·n·(n² − 1)/3 + 100·n
Level 1 2 3 4 5 8 12 15
XP to next 100 150 250 400 600 1,500 3,400 5,350
Cumulative 0 100 250 500 900 3,500 12,100 24,150

Rewards — Seeds are always exactly 2/5 of base XP, so the two economies cannot drift apart during a re-balance:

Trivial Easy Medium Hard Epic
XP 10 25 50 100 200
Seeds 4 10 20 40 80
Attribute points 1 1 2 4 8

A streak multiplies XP and Seeds by 1 + 0.05 × min(streak, 10), up to 1.5×, and the first quest of each day adds a flat +15 XP / +5 Seeds.

Because level is derived from totalXp rather than incremented, one award that crosses three thresholds runs the identical code path as one that crosses none — multi-level-ups are free.


Engineering notes

Three decisions did most of the work.

1. Completing a quest is idempotent three times over

A judge double-clicking "complete" and getting double XP is the most likely visible failure in a product like this, so completion has three independent defences. Any one of them alone prevents the dou AC2C ble award.

  1. An atomic compare-and-set on the task document. Concurrent duplicates serialise; the second re-evaluates the filter against the newly committed document, fails the day-key predicate, and matches nothing. This layer needs no index, so it holds even if index creation failed in production.
  2. A unique-index insert into the completion ledger, keyed taskId:once or taskId:YYYY-MM-DD — derived entirely from server-side facts, never from a client-supplied idempotency header.
  3. A pure $inc on the character (never a read-modify-write, which loses updates under concurrency), followed by a $lt-guarded level write that is monotonic and convergent under any interleaving.

The order is claim → ledger → increment, so a crash mid-way under-awards rather than over-awards. A missed award is invisible; a doubled one is a visible cheat.

A duplicate returns 200 { awarded: false }, not a 4xx — double-clicking is a normal human action, not an error worth a red toast.

2. XP is never taken away

Un-completing a quest puts it back on the board and deducts nothing. The ledger row survives (flagged reverted), so its award key survives too — re-completing the same quest the same day collides on the unique index and pays nothing.

That single decision closes the toggle-farm exploit and removes the whole class of bugs that come from a progress bar running backwards: negative widths, negative ranges, a level number that decreases mid-animation.

3. A "day" is the user's local civil date

Streaks are keyed to YYYY-MM-DD in the user's IANA timezone, never UTC. UTC is wrong in both directions: for an IST user, completions at 11pm and 1am the next evening fall on the same UTC day (under-counts); for a US/Pacific user, a 5pm completion already belongs to the next UTC day (over-counts).

The streak is written once, inside the same atomic update that grants the XP, but decayed at read time. Showing the stored value raw would tell someone who last acted five days ago that they still have a five-day streak — wrong data that looks deliberate, which is worse than a visible error. Decaying on read needs no cron job and no extra query.

Other things worth knowing

  • The client cannot influence an award. POST /api/tasks/:id/complete takes an empty body by design. Every zod schema is .strict(), so {"xp": 99999} is rejected with a 400 rather than silently ignored. Persistence always builds an explicit $set; there is no {...body} spread anywhere in the codebase.
  • Ownership lives in the query filter, never in an if after a fetch. Another user's quest matches zero documents, which is indistinguishable from "not found" — so probing leaks nothing.
  • No endpoint can return HTML. Every handler goes through withRoute(), which maps zod → 400, CastError → 404, duplicate key → 409 and everything else → a generic 500. This is the main anti-blank-screen measure: a fetch expecting JSON never receives Next's error page.
  • GET /api/character upserts. A 30-day session can outlive its documents; without this, such a user reaches the dashboard logged in and finds nothing. With it, the dashboard is renderable for any authenticated user, unconditionally.
  • Themes are real persistence. Equipping one writes to MongoDB and the root layout repaints every surface from the new palette. No primary state is kept in localStorage.

Architecture

A single Next.js App Router monolith. Server Components read MongoDB directly — no fetch to our own API, no useEffect data fetching. Mutations go through REST route handlers, which keeps every endpoint independently testable.

src/
  app/
    page.tsx              public landing (SEO)
    (auth)/               login · signup
    (app)/                authed shell — desk · quests · shop · journal
    api/                  route handlers
    error.tsx · global-error.tsx · not-found.tsx · loading.tsx
  components/             ui/ · layout/ · forms/ · features/
  lib/
    game/                 constants.ts · progression.ts   ← pure, no I/O
    services/             character · tasks · completion · shop
    db/models/            User · Character · Task · TaskCompletion · DailyActivity
    api/                  response envelope · withRoute wrapper

Authorization is enforced in layers. middleware.ts handles redirect UX using an edge-safe config (no Mongoose, no bcrypt), but it is not the security boundary — every server component and route handler independently resolves the session, because a mistake in the middleware matcher would silently disable it.

Indexes

Collection Key
users { email: 1 } unique — duplicate signup is a 409, never a race
characters { userId: 1 } unique — 1:1 guaranteed at the database
tasks { userId: 1, status: 1, createdAt: -1 } the main list query (ESR order)
tasks { userId: 1, category: 1 } category filters
tasks { userId: 1, dueAt: 1 } partial — most quests have no due date
taskcompletions { userId: 1, awardKey: 1 } unique — the idempotency index
taskcompletions { userId: 1, completedAt: -1 } history, cursor-paginated
taskcompletions { userId: 1, dayKey: 1 } per-day aggregates
dailyactivities { userId: 1, dayKey: 1 } unique — makes "first of day" race-free

The completion ledger is referenced, not embedded: it grows without bound. The shop inventory is the only embedded array, and only because the catalogue is a fixed constant, so it is provably bounded.


API

Every response uses one envelope:

{ "ok": true,  "data": { … } }
{ "ok": false, "error": { "code": "VALIDATION_ERROR", "message": "…", "details": { "title": ["…"] } } }
Method Path
POST /api/auth/register creates user + character together
* /api/auth/[...nextauth] sign in / out / session
GET PATCH /api/character GET upserts; PATCH takes only displayName, timezone
POST /api/character/equip 403 if you don't own it
GET POST /api/tasks cursor-paginated; limit clamped to 50 server-side
GET PATCH DELETE /api/tasks/:id status limited to active|archived; DELETE is soft
POST /api/tasks/:id/complete empty body by design
POST /api/tasks/:id/uncomplete character unchanged
GET /api/history the award ledger
GET POST /api/shop · /api/shop/purchase
GET /api/health { db, latencyMs }

Verification

npm run verify               # typecheck + lint + game maths + colour contrast
npm run build
bash scripts/api-smoke.sh    # 43 checks over real HTTP against a running server

verify:progression covers the level table, multi-level jumps, attribute boundaries, timezone-sensitive day keys, and every streak transition including a clock moving backwards — 132 assertions, no database needed.

verify:contrast checks every colour role against WCAG AA (4.5:1) in all five themes, including white-on-accent for buttons. It found real failures on the first run — the currency pill was at 1.92:1 — and exposed a design conflict: dark themes use a light accent, so white button text was unreadable there. That needed a dedicated --color-on-accent token rather than a colour tweak. Run it with --fix to adjust failing colours by lightness alone, which preserves the hue.

verify:a11y audits server-rendered HTML for heading order, landmarks, unlabelled controls and form fields, and layout hazards at phone width:

curl -s http://localhost:3000/desk > /tmp/desk.html && npm run verify:a11y /tmp/desk.html

api-smoke.sh is the one that matters for correctness under abuse. It requires npm run dev on port 3100 and asserts, among other things:

  • six concurrent completions of one quest award exactly once (+210 XP, not +1,260);
  • three concurrent purchases of one item deduct exactly 40 Seeds and never go negative;
  • complete → uncomplete → complete pays nothing the second time;
  • another user's quest returns 404, not 403 and not the document;
  • {"xp": 99999} is rejected, not ignored;
  • a malformed ObjectId, an empty body and broken JSON each return a clean 4xx, never a 500;
  • the history ledger survives deleting the quest it came from;
  • no endpoint returns HTML to a caller expecting JSON, errors included.

Stopping MongoDB mid-session is also worth doing by hand: APIs return 503 JSON within ~5s (never a hang, never HTML), pages return 200 with a server-rendered explanation rather than a blank screen, and the app recovers on its own when the database comes back — no restart.


Environment

Variable Required
MONGODB_URI yes MongoDB Atlas connection string
AUTH_SECRET yes ≥32 chars — openssl rand -base64 32
AUTH_URL production public origin; inferred automatically on Vercel
SEED_DEMO_EMAIL / SEED_DEMO_PASSWORD / SEED_DEMO_TIMEZONE no demo account overrides

.env is gitignored and must never be committed. Environment validation is lazy, so a missing variable surfaces as a clean 503 at request time rather than crashing the build.

Deploying

  1. Push to GitHub and import the repo on Vercel (or any Node host).
  2. Set MONGODB_URI, AUTH_SECRET and AUTH_URL.
  3. In Atlas → Network Access, allow your host's egress (0.0.0.0/0 for Vercel's dynamic IPs).
  4. Open /api/health on the deployed URL — not localhost — to confirm the production backend reaches Atlas before you demo anything.
  5. Optionally run npm run seed against the production URI to create the demo account.

Stack

Next.js 16 (App Router) · React 19 · TypeScript · Tailwind CSS v4 · MongoDB Atlas via Mongoose · Auth.js v5 · zod · Motion. No AI, no external APIs — every number is computed from the rules above.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0