Skilled.dev Developer Docs
This page is the web-accessible local setup guide. File-based version: /docs/README.md
Architecture
Skilled.dev runs as a Next.js web app backed by Hasura + Postgres. The web client sends GraphQL calls to /api/graphql, and that route forwards requests to Hasura with role-aware headers.
src/: Next.js app, API routes, UI, lesson contenthasura/: docker config, migrations, metadata, and the course seed
Prerequisites
- Node.js 24 (
.nvmrc) + npm - Docker + Docker Compose
- Hasura CLI v2.48.10, the version
hasura/docker-compose.ymlruns:curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | VERSION=v2.48.10 bash - macOS or Linux (on Windows, WSL)
Install Dependencies
npm ciEnvironment Variables
Create local app env:
npm run setup:envIt copies .env.example to .env.local and generates a JWT_SECRET and an ENCRYPTION_KEY, and never replaces a value you've set. By hand, generate each with openssl rand -base64 64 | tr -d '\n'.
The variables the app needs:
APP_STAGE=development
ROOT_DOMAIN=localhost
ROOT_URL=http://localhost:3000
GRAPHQL_URL=http://localhost:8080/v1/graphql
HASURA_GRAPHQL_ADMIN_SECRET=<the local-only value in hasura/docker-compose.yml>
JWT_TOKEN_NAME=stoken-local
JWT_SECRET=<64-byte base64>
ENCRYPTION_KEY=<64-byte base64>
OPEN_ALL_LESSONS=trueOPEN_ALL_LESSONS=true opens every lesson to everyone, logged in or not, so you can read and run the whole course without setting up login.
Hasura + Postgres
Start local services:
docker compose -f hasura/docker-compose.yml up -dHealth check:
curl http://localhost:8080/healthzIf port 8080 is taken, start it with HASURA_PORT=8090 in front, and use that port in GRAPHQL_URL and HASURA_GRAPHQL_ENDPOINT.
Migrations + Seeds
Set CLI env vars first. The admin secret is the local-only one from hasura/docker-compose.yml:
export HASURA_GRAPHQL_ENDPOINT=http://localhost:8080
export HASURA_GRAPHQL_ADMIN_SECRET=changeme_changeme_changeme_changeme_changeme_changeme_changeme_changeme_changeme_changemeApply the schema and metadata, then load the course's categories and lessons:
hasura --project hasura migrate apply --database-name default --skip-update-check
hasura --project hasura metadata apply --skip-update-check
node scripts/course-seed.js applyhasura/seeds/course.json is the source of truth for those rows: each lesson's title, order, access level and published status. To change one, edit the file and run node scripts/course-seed.js apply (diff shows what would change). See “Change a Lesson's Title, Order or Access” in docs/README.md.
Run Web App
npm run dev- Web app:
http://localhost:3000/course - Hasura console:
http://localhost:8080/console
With the app running, npm run check:setup checks every step on this page and says how to fix the first one that isn't done, then runs a lesson's code through the app in each language you have images for.
Run Lesson Code
The code editor in each lesson runs code on your machine, in Docker, in JavaScript, TypeScript, Python, Java, C++, C#, Go, Kotlin, Swift or Rust. Nothing to sign up for. Pull or build the images for the languages you'll use, once:
npm run runner:images javascript python java # or with no names, all tenEach run gets a new container with no network, a read-only filesystem and limits on CPU, memory, processes and output. All ten images take about 13 GB of disk, most of it Swift. If a language's image is missing, the editor says which command to run.
This is for running your own code on your own machine. To run strangers' code on a public server, use the runner service (docs/runner-service.md) or Judge0. See docs/README.md for the full table of images, and docs/adding-a-language.md to add a language.
Without the runner images, JavaScript, TypeScript and Python can run with this machine's own Node.js and Python: set CODE_RUNNER=system in .env.local and restart npm run dev. There's no sandbox (programs run as you, with your files and the network), so it only runs code for a browser on the same machine, with the app at localhost. See “Running Code Without Docker” in docs/README.md.
Optional Integrations
- Logging in: an OAuth app (
GITHUB_*,GOOGLE_*orFACEBOOK_*), plusJWT_SECRETandENCRYPTION_KEY. GitHub's is quickest; its callback URL ishttp://localhost:3000/api/auth/github/callback. Stripe isn't needed. - Stripe:
STRIPE_PUBLISHABLE_KEY,STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET - Running code elsewhere than local Docker: the runner service (
CODE_RUNNER=service,RUNNER_SERVICE_URL,RUNNER_SERVICE_SECRET) or Judge0 (JUDGE0_API_KEY,JUDGE0_CALLBACK_SECRET, public callback URL) - Email:
SENDINBLUE_API_KEY,INTERNAL_NOTIFICATION_EMAIL
Tests and Checks
Before opening a pull request, run what CI runs:
npm run ts # typecheck
npm test # unit tests
npm run build # needs Hasura with the course seednpm run conformance runs every lesson's reference solutions and starting code in every language, in Docker (npm run conformance -- -t count-islands for one lesson). After changing a lesson's runner.spec.ts, run npm run runner:generate.
Troubleshooting
Start with npm run check:setup, which finds most of these and says what to run.
Missing GraphQL proxy config
Ensure GRAPHQL_URL and HASURA_GRAPHQL_ADMIN_SECRET are set in .env.local.
Invalid JWT secret
Build/runtime errors requiring a valid JWT_SECRET mean your value is not a 64-byte base64 string, often because openssl rand -base64 64 split it over two lines. Delete its line from .env.local and run npm run setup:env, or regenerate with:
openssl rand -base64 64 | tr -d '\n'Hasura auth/connection failures
Validate local service and env vars:
curl http://localhost:8080/healthz
echo "$HASURA_GRAPHQL_ENDPOINT"
echo "$HASURA_GRAPHQL_ADMIN_SECRET"