A Solana-native learning platform with on-chain credentials.
Learners enroll in courses, complete lessons, and earn credentials that live on-chain:
soulbound XP as Token-2022, course certificates as Metaplex Core NFTs.
Built by Superteam Brazil β’ MIT β’ currently running on devnet
Architecture β’ Features β’ Tech Stack β’ Quickstart β’ Environment β’ Docs
Four moving parts, each with a clear job:
| Part | What it is | Where |
|---|---|---|
| Web app | Next.js 15 App Router on Vercel | apps/web/ |
| On-chain program | Pinocchio (Rust), 18 instructions, 6 PDA types | onchain-academy/ |
| Database | Supabase Postgres β 36 tables, RLS on all of them | supabase/ |
| Course content | A committed bundle, compiled from a separate repo | solanabr/academy-courses |
ββββββββββββββββββββββββββββββββ
learner's wallet βββ Browser β
(SIWS / embedded) β React Β· Monaco Β· QuickJS β
βββββββββββββββββ¬βββββββββββββββ
β
βββββββββββββββββΌβββββββββββββββ
β Next.js 15 (Vercel) β
β server components β
β 72 API routes β
β ββββββββββββββββββββββββββ β
β β committed content β β
β β bundle β no CMS β β
β ββββββββββββββββββββββββββ β
βββββ¬βββββββββββ¬ββββββββββββ¬ββββ
β β β
ββββββββββΌβββ ββββββΌββββββ ββββΌβββββββββββ
β Supabase β β Solana β β Build serverβ
β DB + Auth β β devnet β β Cloud Run β
βββββββββββββ ββββββ¬ββββββ βββββββββββββββ
β² β
βββββββββββββββ
Helius webhook replays
on-chain events into the mirror
Two invariants shape everything:
- On-chain is the source of truth for XP balances, lesson completion (a bitmap in the Enrollment PDA), and credentials. Supabase mirrors it for fast queries, streaks, and leaderboards; mirror writes are non-fatal and rebuildable.
- There is no CMS. Content is authored in git, compiled ahead of time by
compile-content.ts, and committed to this repo as typed JSON pinned to oneacademy-coursescommit (apps/web/content.lock). Nothing fetches content at runtime, and no credential in the app can mutate it β publishing is a pull request.
On-chain credentials
- Soulbound XP β Token-2022 with NonTransferable + PermanentDelegate. Minted by CPI on lesson completion; cannot be transferred or self-burned.
- Credential NFTs β Metaplex Core, frozen to the learner's wallet via PermanentFreezeDelegate, one collection per course track.
- On-chain lesson tracking β a 256-bit bitmap in the Enrollment PDA, one bit per lesson slot.
Interactive learning
- A lesson is an ordered
blocks[]page builder: prose, video, Monaco code challenges, quizzes, reflections, and Solana widgets (devnet airdrop, IDL program explorer, deployed-program card). buildableRust/Anchor challenges and in-browser program deploys compile through a Rust/Axum build server on Cloud Run.- An AI lesson assistant (Gemini-backed) with per-day spend caps enforced by a Postgres ledger.
Gamification
- 18 achievements β a course-agnostic ladder plus one course badge β with unlock rules declared in content, not code.
- 5 daily/multi-day quests, streaks with freezes, leagues, referrals, and a leaderboard (all-time, weekly, monthly, cohort).
- Level is
floor(sqrt(totalXP / 100)). Reward popups are queued and capped at three cards per moment.
Community and platform
- Forum threads, answers, voting, accepted answers, a flag queue, and XP for participation with a daily cap.
- Three languages: English, Portuguese (pt-BR), Spanish.
- Dark/light mode on the "ink" visual system.
- An admin console for publishing content pins, deploying courses on-chain, moderation, and insights.
| Layer | Technology |
|---|---|
| Frontend | Next.js 15 (App Router), React 18, Tailwind CSS, shadcn/ui + Radix UI |
| Content | Committed bundle compiled from the academy-courses git repo |
| Database / Auth | Supabase (Postgres, RLS, Auth) |
| On-chain program | Solana, Pinocchio 0.11 (Rust, cargo build-sbf) |
| XP tokens | Token-2022 (NonTransferable + PermanentDelegate) |
| Credential NFTs | Metaplex Core (soulbound via PermanentFreezeDelegate) |
| Auth | SIWS + Dynamic embedded wallets + Supabase OAuth (Google, GitHub) |
| i18n | next-intl (EN, PT-BR, ES) |
| Code editor | Monaco Editor, QuickJS sandbox for the challenge runner |
| Build server | Rust/Axum on GCP Cloud Run |
| AI assistant | Gemini over raw REST, spend-metered in Postgres |
| RPC / indexing | Helius (DAS API, webhooks) |
| Storage | Arweave via Irys (permanent credential metadata) |
| Analytics | GA4, PostHog, Sentry (all optional) |
| Monorepo | Turborepo + pnpm 10 |
| Deployment | Vercel (web) + GCP Cloud Run (build server) |
- Node.js >= 18 and pnpm 10 (the repo
pins
packageManager) - A Supabase project (free tier is fine)
- A Solana wallet (Phantom works)
For on-chain work you also need Rust, the
Solana CLI, and cargo build-sbf.
No Anchor CLI β the program is Pinocchio.
# 1. Clone and install
git clone https://github.com/solanabr/superteam-academy.git
cd superteam-academy
pnpm install
# 2. Configure the environment
cp .env.example apps/web/.env.local
# .env.example holds illustrative defaults, not working credentials β replace
# every placeholder. Minimum to boot: NEXT_PUBLIC_SUPABASE_URL,
# NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY,
# NEXT_PUBLIC_SOLANA_RPC_URL, SOLANA_RPC_URL.
# 3. Set up the database β migrations are the source of truth
supabase link --project-ref <your-project-ref>
supabase db push # applies supabase/migrations/ in order
# supabase/schema.sql is a generated snapshot for diffing β never run it directly.
# 4. Content β nothing to import.
# The bundle is already committed at apps/web/src/content/generated/.
# To rebuild it after bumping the pin:
# pnpm --filter web compile-content
# 5. Start the dev server
pnpm devOpen http://localhost:3000.
Nothing shows up on a fresh project. Course visibility is gated on the Supabase
onchain_deploymentstable (status = "synced"and active), which starts empty. Deploy courses from/en/adminβ see docs/ADMIN.md.
Full on-chain features additionally need NEXT_PUBLIC_PROGRAM_ID,
NEXT_PUBLIC_XP_MINT_ADDRESS, PROGRAM_AUTHORITY_SECRET, and
BACKEND_SIGNER_SECRET β see docs/DEPLOY-PROGRAM.md
for the deploy-and-initialize workflow.
pnpm dev # Next.js dev server
pnpm build # production build (Turborepo)
pnpm lint # ESLint
pnpm typecheck # tsc
pnpm format # Prettiersuperteam-academy/
βββ apps/
β βββ web/ # Next.js 15 app
β β βββ src/app/[locale]/ # pages β (marketing), (platform), admin
β β βββ src/app/api/ # 72 API routes
β β βββ src/lib/ # content, solana, auth, gamification, ai, heliusβ¦
β β βββ src/content/generated/# COMMITTED content bundle β never hand-edit
β β βββ src/messages/ # i18n catalogs (en, pt-BR, es)
β β βββ scripts/ # compile-content.ts (content repo β bundle)
β β βββ content.lock # the academy-courses commit the bundle is pinned to
β βββ build-server/ # Rust/Axum program compiler (Cloud Run)
βββ onchain-academy/ # Pinocchio program workspace + IDL + tests
βββ packages/
β βββ types/ # shared TypeScript interfaces
β βββ content-schema/ # Zod schemas for the content standard
β βββ content-lint/ # content linter β runs in academy-courses CI
β βββ challenge-executor/ # sandboxed challenge runner (QuickJS)
β βββ deploy/ # browser-side Solana program deployment
β βββ config/ # shared ESLint / TS / Tailwind configs
βββ supabase/ # migrations (source of truth) + schema snapshot
βββ scripts/ # operator scripts
βββ wallets/ # keypairs (gitignored)
βββ docs/ # the guides listed below
Course content lives in a separate repo:
solanabr/academy-courses.
Copy .env.example to apps/web/.env.local and fill in values. Required
variables are validated at boot β a missing one fails loudly rather than
degrading silently.
The authoritative, annotated list β every variable, what it does, and what happens when it is unset β is the
## Environment Variablesblock inapps/web/CLAUDE.md. The tables below are the shape of it, not a replacement.
| Variable | Scope | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
Client | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Client | Public anon key (safe for the browser) |
SUPABASE_SERVICE_ROLE_KEY |
Server | Service role key. Never exposed to the browser; also gates admin auth |
NEXT_PUBLIC_SOLANA_RPC_URL |
Client | Browser RPC. Must carry no privileged key, or a domain-restricted Helius key |
SOLANA_RPC_URL |
Server | Server RPC β this is the one that may carry an unrestricted Helius key |
NEXT_PUBLIC_APP_URL |
Client | Base URL for sitemap, OG tags, and NFT metadata URIs. A production build fails without it |
| Variable | Scope | Description |
|---|---|---|
NEXT_PUBLIC_PROGRAM_ID |
Client | Deployed program id |
NEXT_PUBLIC_XP_MINT_ADDRESS |
Client | XP mint pubkey, from the initialize output |
NEXT_PUBLIC_SOLANA_NETWORK |
Client | Network name (devnet) |
PROGRAM_AUTHORITY_SECRET |
Server | Authority keypair (JSON array of 64 bytes) β the one that signed initialize |
BACKEND_SIGNER_SECRET |
Server | Rotatable backend co-signer. On devnet, the same as the authority |
XP_MINT_AUTHORITY_SECRET |
Server | XP mint authority. Omit to disable XP minting |
There is no admin password. Admin access is the caller's Supabase session
checked against the admin_users allowlist; the old ADMIN_SECRET is retired.
| Set these for⦠| Variables |
|---|---|
| Helius event ingestion | HELIUS_API_KEY, HELIUS_WEBHOOK_SECRET |
| The admin publish card | GITHUB_TOKEN β read scope only; unset β that card 503s |
| Rust builds and deploys | BUILD_SERVER_URL, BUILD_SERVER_API_KEY, RUST_PLAYGROUND_URL |
| The AI lesson assistant | GEMINI_API_KEY, AI_PARTNER_SEAL_SECRET, the AI_SPEND_* caps, the AI_MODEL_* overrides |
| Embedded wallets | NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID |
| Permanent credentials | ARWEAVE_UPLOADER_SECRET β a Solana keypair funding Irys, not an Arweave JWK |
| Outbound email + cron | RESEND_API_KEY, EMAIL_FROM, CRON_SECRET β fail-closed: unset means nothing is sent |
| Moderation notifications | MODERATION_WEBHOOK_URL |
| Teacher course preview | TEACH_PREVIEW_PASSWORD β unset disables the preview |
| Analytics | NEXT_PUBLIC_GA4_MEASUREMENT_ID, NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST, NEXT_PUBLIC_SENTRY_DSN |
NEXT_PUBLIC_*values are inlined at build time. Changing one requires a redeploy with "Use existing Build Cache" disabled β a cache-reusing redeploy keeps the old value baked into the served chunks.
Three ways in, all of which end at a server-set Supabase cookie session β nothing mints a session client-side:
| Rail | What proves the user | Exchanged at | Optional? |
|---|---|---|---|
| SIWS, external wallet | The wallet signs a server-issued nonce | /api/auth/wallet |
No β the guaranteed path and the kill switch |
| Dynamic embedded wallets | Dynamic proves the Google handshake + wallet ownership | /api/auth/dynamic |
Yes β unset NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID |
| Supabase OAuth | Google or GitHub, through Supabase Auth | /api/auth/callback |
Yes β the fallback rail |
The full map, including the post-login rituals every rail must share, is docs/AUTH-FLOWS.md.
Built with Pinocchio 0.11 (#![no_std], cargo build-sbf). The Anchor
implementation it was ported from has been deleted β there is no Anchor.toml
and no Anchor CLI in the toolchain. The committed IDL at
onchain-academy/idl/onchain_academy.json is still Anchor IDL format, because
that is the wire contract every client decodes against.
| Surface | Count | Detail |
|---|---|---|
| Instructions | 18 | dispatched on the 8-byte sha256("global:<name>") sighash |
| PDA types | 6 | Config, Course, Enrollment, MinterRole, AchievementType, AchievementReceipt |
| Errors | 37 | 6000+, mapped back to i18n keys client-side |
| Events | 18 | emitted byte-identically to Anchor's emit!, so existing indexers keep working |
The instruction set covers the whole learning lifecycle: initialize and config, course CRUD, enroll and close enrollment, complete lesson, finalize course, issue and upgrade credential, register/update/revoke minter, reward XP, and the achievement type/award pair.
The program id is baked at compile time in two flavors. The default build carries
the upstream id 7NeJaSRyb4Wxay3Tcd9bdpD7T3GWYUQSFyrhG8SgwE8V (what the IDL
declares); --features fresh-id carries
Dsro2Cd9Mhgk8L71imh3LLPwYU5PU8hvBY5HEcPrcx5u, the self-owned devnet instance
that is actually deployed and initialized. Clients read the id from
NEXT_PUBLIC_PROGRAM_ID and throw if it is unset.
Byte-level layouts, validation order, and every invariant: docs/SPEC.md. Deploying your own instance: docs/DEPLOY-PROGRAM.md.
URL: /{locale}/admin (e.g. /en/admin)
Auth: your ordinary Supabase session, checked against an admin_users
allowlist that only the service role can read. There is no admin password and no
login form β signed-in non-admins get a 404.
| Screen | What it does |
|---|---|
| Courses | The content pin vs academy-courses HEAD, a prefilled publish PR link, on-chain course/achievement deploys, and activate/deactivate |
| Moderation | The pending community-flag queue |
| Insights | Platform-behaviour aggregates β AI-tutor usage, spend, lesson funnel |
| Status | Program liveness, authority match, deploy counts, and on-chain β Supabase resync |
Publishing is a pull request β the console holds no write token and cannot
mutate content. Deploys are recorded in onchain_deployments, which is the
learner-visibility gate. See docs/ADMIN.md.
| Doc | What it covers |
|---|---|
| ARCHITECTURE.md | System design, data flows, service interfaces, DB schema |
| AUTH-FLOWS.md | Every way into a session, mapped from the code |
| SPEC.md | Authoritative on-chain program specification |
| DEPLOYMENT.md | Vercel, Supabase, Cloud Run, cron, analytics |
| DEPLOY-PROGRAM.md | Build, deploy, and initialize the program on devnet |
| ADMIN.md | The admin console, screen by screen |
| CUSTOMIZATION.md | Theming, i18n, extending content and gamification |
| PINOCCHIO-MIGRATION.md | What changed in the Anchor β Pinocchio port |
| DB-MIGRATION-LEDGER.md | Filename β prod migration-ledger reconciliation |
| CLAUDE.md | Codebase conventions, security model, env-var reference |
Branch, commit conventionally, open a PR:
git checkout -b feat/your-thing
git commit -m "feat: add quiz lesson type"House rules that CI actually enforces:
- TypeScript strict, zero
any - All UI strings through next-intl β never hardcoded in components
- Server components by default; client components only where needed
- ESLint + Prettier on a pre-commit hook
- A byte-comparison of the committed content bundle against a fresh recompile
MIT.
Thanks to Superteam Brazil and the Solana Foundation.