Issues Open Badges 3.0 credentials from Koski study records, with passkey-authenticated collections and sharing.
Koski is Finland's national registry of study records. opintotodiste lets a person paste a link to their own Koski data, converts the eligible records into draft Open Badges 3.0 credentials via koski2openbadge, and from there signs, delivers, or archives them.
Signing and wallet delivery are delegated to two companion services rather than implemented here:
digitalcredentials/signing-serviceturns a draft badge into a signed Verifiable Credential.FindyFi/oid4vci-issuerhands a signed credential to a wallet app over OID4VCI (pre-authorized-code flow, QR code + deep link).
opintotodiste itself owns the Koski import, the signing/delivery UI, and its own persistence layer: PostgreSQL for user accounts, WebAuthn passkeys, and named, shareable collections of credentials.
Requires Node.js 20+, PostgreSQL, and reachable signing-service/oid4vci-issuer instances. The included docker-compose.yml provides all of these, and needs no sibling checkouts — a fresh clone is enough:
git clone https://github.com/FindyFi/opintotodiste.git
cd opintotodiste
docker compose up --buildTo install and run without Docker instead:
npm install
cp .env.example .env # then edit as neededkoski2openbadge — the converter that turns Koski study records into Open Badges achievement data — is not published to npm. package.json depends on it straight from GitHub, and package-lock.json pins the exact commit that gets installed.
A few consequences worth knowing before deploying:
- No git binary or SSH key is needed, to install or to build the image. The
resolvedURL inpackage-lock.jsonreadsgit+ssh://, which looks alarming in a Dockerfile, but npm recognises GitHub specs and fetches them over HTTPS through the tarball endpoint. It does mean the repository has to stay publicly readable — if it is ever made private, every build breaks. - The lockfile is the pin.
npm ciinstalls exactly the commit recorded there, so builds are reproducible;npm installwithout a lockfile would silently take whatever the default branch points at. Runnpm update koski2openbadgeto move to a newer commit deliberately, and commit the result. - Prefer a tagged ref in
package.json(github:FindyFi/koski2openbadge#v0.1.0) over the baregithub:FindyFi/koski2openbadge, so that what is being depended on is readable without decoding a commit hash.
It is a plain library with no runtime dependencies and no service of its own, so there is nothing to deploy for it separately — it ships inside this app's image.
With Docker, the app is available at http://localhost:3000 once docker compose up is running.
Without Docker (after npm install, with .env configured and Postgres/signing-service/oid4vci-issuer reachable):
npm start- Koski import — fetches a person's study records from the Koski API and converts them into draft Open Badges.
- Signing & wallet delivery — signs a draft badge and/or pushes it to a wallet app, via the two companion services described in Background.
- Collections — signed-in users can save selected credentials into named collections and share them via a public, unauthenticated link.
- Passkey authentication — account registration and sign-in use WebAuthn passkeys, not passwords.
- Bilingual UI — Finnish/English, via
translate-element.
All configuration is via environment variables — see .env.example for the full list with explanations. In short:
| Variable | Purpose |
|---|---|
PORT |
Port the app listens on |
SESSION_SECRET |
Express session signing secret |
DATABASE_URL |
Postgres connection string |
RP_ID, RP_NAME, ORIGIN |
WebAuthn relying-party config for passkey login |
ISSUER_ID, ISSUER_NAME |
Identity used as the issuer on issued credentials |
SIGNING_SERVICE_URL, SIGNING_SERVICE_INSTANCE_ID, SIGNING_SERVICE_SUITE |
Where/how to reach signing-service |
OID4VCI_ISSUER_URL, OID4VCI_ISSUER_INSTANCE, OID4VCI_ISSUER_TOKEN |
Where/how to reach oid4vci-issuer |
npm testUnit tests for the Koski import, credential building and the two service
clients run with no external dependencies. The tests that exercise the
running app need PostgreSQL, because index.js connects to it at import
time, and report themselves as skipped without it:
docker compose up -d db
npm testSkipping is a convenience for a bare clone and a trap anywhere that is meant
to be testing something: node:test reports a skipped describe as zero tests
rather than as a skip, so an unreachable database would leave a green run
that exercised nothing. Set REQUIRE_POSTGRES=1 to turn that into a failure:
REQUIRE_POSTGRES=1 npm testCI sets it, and runs the suite on Node 20, 22 and 24 against a real PostgreSQL service. It also builds the image on every pull request, since that is what gets deployed.
This repository builds a container image from its Dockerfile
and nothing more: it does not publish images to any registry, and it carries
no environment-specific or infrastructure configuration. Everything the app
needs at runtime comes from the environment variables listed under
Configuration.
Publishing is separate on purpose: the image is built from this
repository's Dockerfile and pushed to a registry elsewhere, and the
credentials for that live with it rather than here. Keeping registry access
out of a public repository is the point, so please do not add a push workflow
back into this one. Deploying a newly built tag — updating the
image reference for an environment and applying it — is a further step again,
and neither this repository nor the build does it.
Two things follow for anyone changing this repository:
- The
Dockerfileis the release artifact. If it cannot build from a clean clone, no image can be published.package-lock.jsonis committed becausenpm cirequires it — and because it is what pins the exactkoski2openbadgecommit that goes into the image (see Install). - Pin a tag, not a branch. Both this repository and
oid4vci-issuershould be consumed at a released tag, so a merge cannot silently change what the next build produces.
See CONTRIBUTING.md. Issues and pull requests are welcome at FindyFi/opintotodiste. Security reports go through the process in SECURITY.md rather than the issue tracker.
Apache-2.0 © FindyFi