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 content
  • hasura/: 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.yml runs:
    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 ci

Environment Variables

Create local app env:

npm run setup:env

It 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=true

OPEN_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 -d

Health check:

curl http://localhost:8080/healthz

If 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_changeme

Apply 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 apply

hasura/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 ten

Each 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_* or FACEBOOK_*), plus JWT_SECRET and ENCRYPTION_KEY. GitHub's is quickest; its callback URL is http://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 seed

npm 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"