lti-example-tool is a Gleam-based LTI 1.3 tool provider application. It exposes endpoints for OIDC login, LTI launch validation, AGS score submission, NRPS memberships, JWKS publishing, and platform registration management.
Primary goals of the app:
- Demonstrate LTI 1.3 tool/provider integration patterns.
- Provide a working local + Docker development workflow.
- Store platform registrations/deployments and cryptographic state in PostgreSQL.
Request and runtime flow:
src/lti_example_tool.gleamstarts Mist + Wisp and builds app context viaapplication.setup().src/lti_example_tool/application.gleam:- Loads env/config.
- Ensures DB exists and is migrated/seeded.
- Opens DB connection pool.
- Builds LTI data provider.
- Builds app context for controllers/views.
src/lti_example_tool_web/router.gleamdispatches requests by path segments.src/lti_example_tool_web/web.gleamapplies middleware (logging, crash rescue, static files, HEAD handling).- Controllers render HTML with Nakai (
src/lti_example_tool_web/html*) or JSON for JWKS.
Design style:
- No router DSL; explicit pattern matching on path/method.
Result-driven control flow (use ... <- result.try(...)style).- Lightweight server-rendered HTML + Tailwind CSS.
- App startup:
src/lti_example_tool.gleam,src/lti_example_tool/application.gleam - Request routing:
src/lti_example_tool_web/router.gleam - Middleware + URL helpers:
src/lti_example_tool_web/web.gleam - LTI endpoints:
src/lti_example_tool_web/controllers/lti_controller.gleam - Registration CRUD:
src/lti_example_tool_web/controllers/registration_controller.gleam - DB connection + transaction helpers:
src/lti_example_tool/database.gleam - Migrations/seeding CLI:
src/lti_example_tool/database/migrate.gleam - Seeds loader (
seeds.yml):src/lti_example_tool/seeds.gleam - LTI data provider adapters:
src/lti_example_tool/db_provider.gleam - Security data stores:
- JWKs:
src/lti_example_tool/jwks.gleam - Nonces:
src/lti_example_tool/nonces.gleam
- JWKs:
- Feature flags:
src/lti_example_tool/feature_flags.gleam - Logger + FFI:
src/lti_example_tool/utils/logger.gleam,src/lti_example_tool_ffi.erl
- Tool versions (from
.tool-versions) - PostgreSQL
watchexecfor local auto-reloadgooseCLI for SQL migrations
asdf installnpm installgleam deps downloadcp seeds.example.yml seeds.yml(edit values)gleam run -m lti_example_tool/database/migrate setupnpm run dev- Open
http://localhost:8080
docker compose up- App:
http://localhost:8080 - Postgres:
localhost:5432(postgres/postgres)
Default DB URL fallback:
postgresql://postgres:postgres@localhost:5432/lti_example_tool
Core DB initialization behavior:
application.setup()callsmigrate.maybe_initialize_db()on startup.- If DB does not exist, setup creates DB, runs migrations, seeds initial JWK + optional seeds file registrations.
Migrations currently create:
goose_db_versionregistrationsdeploymentsoidc_statesuserstokensnoncesjwksactive_jwk
- Language/runtime: Gleam on Erlang/OTP.
- HTTP stack: Wisp + Mist.
- LTI domain library:
lightbulb(git dependency onmaster). - DB: PostgreSQL via
pog. - Forms:
formal. - HTML rendering:
nakai. - YAML parsing for seeds:
glaml. - CSS: Tailwind (
app.css->priv/static/app.css).
Patterns to preserve:
- Prefer explicit
Resulterror paths over exceptions. - Keep routing and method checks explicit in controllers.
- Use existing repository modules (
registrations,deployments,jwks,nonces) instead of ad-hoc SQL in controllers. - Keep user-facing errors safe; log internals with
logger.error_meta.
- Run app:
gleam run - Run full local dev stack (server reload + Tailwind watch + client rebuild watch):
npm run dev - Run server-only auto-reload:
npm run server:dev - Build app:
gleam build - Build CSS once:
npm run tailwind:build - Watch CSS:
npm run tailwind:watch - Run tests:
gleam test - Format check:
gleam format --check src test - Format code:
gleam format src test - Full clean:
npm run clean
- Setup DB:
gleam run -m lti_example_tool/database/migrate setup - Migrate only:
gleam run -m lti_example_tool/database/migrate up - Seed only:
gleam run -m lti_example_tool/database/migrate seed - Reset DB:
gleam run -m lti_example_tool/database/migrate reset - Setup test DB:
gleam run -m lti_example_tool/database/migrate test.setup - Reset test DB:
gleam run -m lti_example_tool/database/migrate test.reset
Useful SQL checks:
SELECT * FROM goose_db_version ORDER BY id DESC;SELECT * FROM registrations;SELECT * FROM deployments;SELECT * FROM nonces;SELECT * FROM active_jwk;
src/lti_example_tool.gleam: main entrypointsrc/lti_example_tool/: domain, data, app setup, and core modulessrc/lti_example_tool_web/: web layer modules (router, middleware, controllers, HTML views)src/lti_example_tool/database/: DB tooling modulespriv/repo/migrations/: goose SQL migrations (timestamp_name.sql)test/: gleeunit testspriv/static/: built static assetsapp.css: Tailwind inputseeds.example.yml,seeds.yml: seed dataDockerfile,docker-compose.yml: container workflows.github/workflows/: CI (test + package)
GET /home pagePOST /loginOIDC loginPOST /launchlaunch validationPOST /scoreAGS score submitPOST /membershipsNRPS memberships fetchGET /.well-known/jwks.jsonpublic keysGET|POST /registrations...registration CRUD + access token utilities
- Logs are simplified via FFI logger config (
level: messagestyle). - Missing
SECRET_KEY_BASEinProdwarns and defaults to insecure"change_me". PUBLIC_URLcontrols URL generation; default is localhost with detected port./registrationsUI can be disabled by feature flags (ENABLE_REGISTRATIONS).- OIDC failures are often state-cookie related; ensure secure cookie behavior matches your deployment/proxy.
- Seed loading errors usually come from invalid/missing
seeds.ymlfields. - Ensure platform
issuer + client_id + deployment_idcombination matches stored registration/deployment rows.
- CI workflow starts Postgres, runs build,
test.setup,gleam test, and format check. - Container image publish workflow pushes
ghcr.io/simon-initiative/lti-example-tool. - Keep changes CI-safe by running locally:
gleam buildgleam run -m lti_example_tool/database/migrate test.setupgleam testgleam format --check src test
When making changes:
- Read the relevant controller + repository module first.
- If data model changes are required, add migration SQL in
priv/repo/migrations, then update repository decoders and tests. - Keep AppContext wiring centralized in
application.gleam. - Prefer adding/adjusting tests in
test/for new behavior. - Verify with build + tests + formatting before finalizing.
When adding a new endpoint:
- Add route match in
src/lti_example_tool_web/router.gleam. - Add controller action in
src/lti_example_tool_web/controllers/*. - Reuse middleware conventions in
src/lti_example_tool_web/web.gleam. - Add/update view templates in
src/lti_example_tool_web/html/when returning HTML. - Add integration-style test via
wisp/simulate.
When diagnosing DB errors:
- Confirm
DATABASE_URL. - Run
... database/migrate setuporreset. - Check
goose_db_versionand key domain tables. - Validate seed YAML shape if using seed-driven registrations.
- Project intro/setup:
README.md - Dependency/runtime config:
gleam.toml,manifest.toml - LTI flow implementation references:
src/lti_example_tool_web/controllers/lti_controller.gleamsrc/lti_example_tool/db_provider.gleam
- CI reference:
.github/workflows/test.yml