{"id":34569055,"url":"https://github.com/notrab/unlocked","last_synced_at":"2026-03-15T19:10:32.945Z","repository":{"id":313941920,"uuid":"1053407113","full_name":"notrab/unlocked","owner":"notrab","description":null,"archived":false,"fork":false,"pushed_at":"2025-09-09T15:12:35.000Z","size":5,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-24T03:35:53.273Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/notrab.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-09T12:07:15.000Z","updated_at":"2025-09-09T15:12:39.000Z","dependencies_parsed_at":"2025-09-09T18:50:48.119Z","dependency_job_id":"082a9dd5-5b39-4c77-89bb-9fd6891082b2","html_url":"https://github.com/notrab/unlocked","commit_stats":null,"previous_names":["notrab/unlocked"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/notrab/unlocked","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notrab%2Funlocked","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notrab%2Funlocked/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notrab%2Funlocked/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notrab%2Funlocked/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/notrab","download_url":"https://codeload.github.com/notrab/unlocked/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/notrab%2Funlocked/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":27999539,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-24T02:00:07.193Z","response_time":83,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-12-24T09:14:12.987Z","updated_at":"2025-12-24T09:14:13.431Z","avatar_url":"https://github.com/notrab.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# XP for Saas\n\nAdd a game-like XP system to your SaaS app (think Fortnite XP, Xbox Achievements, COD Battle Pass), with a clean, NextAuth-inspired developer experience.\n\nUsers earn XP for actions (profile complete, purchase, daily login, referrals, etc.), progress through tiers, and unlock rewards.\nBring your own database (Redis, Prisma, Postgres, etc.) via adapters.\n\n\n## 1. Install\n\n```bash\nnpm install @notrab/xp\n```\n\n## 2. Define your config\n\nCreate a `xp.ts` file in your app:\n\n```ts\n// lib/xp.ts\nimport { createXP, XPRoutes } from '@notrab/xp/server'\nimport { createRedisAdapter } from '@notrab/xp/adapters/redis-adapter'\nimport { auth } from '@/lib/auth' // NextAuth, Clerk, etc.\nimport { redis } from './redis'\n\n// Define tiers, actions, hooks\nexport const xp = createXP({\n  config: {\n    tiers: [\n      { name: 'Newcomer', threshold: 0, color: '#94a3b8', badge: '🌱' },\n      { name: 'Explorer', threshold: 100, color: '#3b82f6', badge: '🔍' },\n      { name: 'Expert', threshold: 500, color: '#8b5cf6', badge: '⭐' },\n      { name: 'Master', threshold: 1500, color: '#f59e0b', badge: '👑' },\n    ],\n    actions: {\n      profile_complete: { xp: 50, label: 'Complete Profile', unique: true },\n      first_purchase: { xp: 200, label: 'First Purchase', unique: true },\n      daily_login: { xp: 10, label: 'Daily Login', cooldownSeconds: 86400 },\n    },\n    hooks: {\n      onAction: async ({ userId, actionKey, amount }) =\u003e {\n        console.log(`[xp] ${userId} +${amount} XP for ${actionKey}`)\n      },\n      onLevelUp: async ({ userId, toTier }) =\u003e {\n        if (toTier.name === 'Explorer') {\n          await sendDiscountEmail(userId, 'WELCOME10')\n        }\n        if (toTier.name === 'Expert') {\n          await promotePlanUpgrade(userId)\n        }\n      },\n    },\n  },\n  adapter: createRedisAdapter(redis),\n  getUserId: async (req) =\u003e {\n    const session = await auth()\n    return session?.user?.id ?? null\n  },\n})\n\n// Export API routes\nexport const { GET, POST } = XPRoutes(xp)\n```\n\n## 3. API Route\n\nCreate `app/api/xp/route.ts`:\n\n```ts\nexport { GET, POST } from '@/lib/xp'\n```\n\n## 4. Wrap your App\n\n```tsx\n// app/layout.tsx\nimport { XPProvider } from '@notrab/xp'\nimport { xp } from '@/lib/xp'\nimport { useAuth } from '@clerk/nextjs'\n\nexport default function RootLayout({ children }) {\n  const { user } = useAuth()\n  return (\n    \u003chtml\u003e\n      \u003cbody\u003e\n        \u003cXPProvider config={xp.config} userId={user?.id}\u003e\n          {children}\n        \u003c/XPProvider\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  )\n}\n```\n\n## 5. Use in components\n\n```tsx\nimport { XPProgressBar, useXP, useXPAction } from '@notrab/xp'\n\nexport function Dashboard() {\n  const { xp, tier, isLoading } = useXP()\n  const triggerProfileComplete = useXPAction('profile_complete')\n\n  if (isLoading) return \u003cdiv\u003eLoading...\u003c/div\u003e\n\n  return (\n    \u003cdiv\u003e\n      \u003ch1\u003eWelcome back!\u003c/h1\u003e\n      \u003cXPProgressBar /\u003e\n      \u003cp\u003eLevel: {tier} ({xp} XP)\u003c/p\u003e\n      \u003cbutton onClick={() =\u003e triggerProfileComplete()}\u003e\n        Complete Profile (+50 XP)\n      \u003c/button\u003e\n    \u003c/div\u003e\n  )\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotrab%2Funlocked","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnotrab%2Funlocked","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnotrab%2Funlocked/lists"}