Skip to content
edbfiPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

147 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Setun logo

Setun

A privacy-first AI learning environment for classrooms

CI status AGPL-3.0 license Bun 1.4 or newer SvelteKit 3 TypeScript SQLite


What is Setun?

Setun is a self-hosted environment for teaching AI and Teknologiforståelse. Students use pseudonymous access cards instead of accounts, email addresses, or real names. Educators control which models, skills, tools, schedules, and spending limits each classroom receives.

The application combines streaming AI chat with a code artifact workspace, while keeping generated code on a separate, tightly restricted origin. Model credentials stay behind an internal gateway and never reach the browser or the Setun database.

The name comes from the 1958 Setun computer, built around balanced ternary. The mark stacks its three states: minus, zero, and plus.

Features

  • Pseudonymous student access — issue printable access cards without collecting names, email addresses, or third-party identities.
  • Classroom controls — manage rosters, weekly schedules, temporary locks, model allowlists, instructions, retention, and daily or per-student budgets.
  • Streaming AI chat — run classroom-scoped conversations with attachments, search, cancellation, and model aliases chosen by the educator.
  • Live artifacts beside the conversation — preview, edit, rerun, and version HTML, SVG, JSX, TSX, and Svelte creations in a pane that never covers the composer, so a student can look at what the model made while asking for the next change. One control moves the workspace between the conversation, both, and the build. Artifacts are built up over several turns: the model writes an id on the fence (```html id=home-page) and reuses it to revise the same thing, and a run's outcome travels back so the next answer can fix the error the student saw.
  • Curated capabilities — publish reusable skills and expose only approved MCP tools, with per-classroom controls for tools that require confirmation.
  • Image workflows — support image attachments and model-generated images without exposing the underlying storage directory.
  • English and Danish — complete localized interfaces, with a classroom default and per-student override.
  • Light and dark — the interface follows the device by default, with an explicit choice stored in the browser and never against the student.
  • Self-hosted operations — SQLite persistence, automatic migrations, retention jobs, nightly snapshots, and a Compose deployment behind Caddy.

Architecture

flowchart LR
  Browser[Student and educator browsers] -->|app.example.org| Caddy
  Browser -->|artifacts.example.org| Caddy
  Caddy --> App[Setun app]
  Caddy --> Sandbox[Static artifact sandbox]
  App --> SQLite[(SQLite)]
  App --> Storage[(Private file storage)]
  App --> CPA[CLIProxyAPI]
  CPA --> Providers[AI providers]
  App --> MCP[Approved MCP servers]
Loading

The application and artifact sandbox must use different hostnames. That origin boundary is part of the security model: Caddy serves the sandbox as static files with a restrictive content security policy, and generated code has no route back into the authenticated application. CLIProxyAPI is reachable only from the app's internal network and has no published host port.

Tech stack

Layer Technology
Runtime and package manager Bun 1.4+
Application SvelteKit 3 with the official Bun adapter, Svelte 5, TypeScript
UI UnoCSS, shadcn-svelte, bits-ui
Data SQLite with Drizzle ORM
Validation and forms Valibot and sveltekit-superforms
Localization Paraglide JS
Model gateway CLIProxyAPI
Production edge Caddy and Docker Compose
Quality gates svelte-check, Biome, Bun test, Vitest, Playwright, Python, repository hygiene

Quick start

For local development, install:

  • Bun 1.4 or newer
  • Node.js 22.17 or newer (CI uses .node-version) for svelte-kit sync, svelte-check, Vitest and Playwright
  • Python 3.14 for the development suite
  • prek for repository hooks

Then run:

bun install
prek install
./scripts/devsuite start

The suite starts the application, sandbox, and a persistent development database, then prints the student and educator URLs and credentials. Development secrets are generated per instance when they are not present in the environment or .env; the suite never writes them to .env.

The default application is at http://localhost:5173, with educator login at http://localhost:5173/educator/login. The sandbox runs separately on port 5174. Pressing Ctrl-C stops an attached stack while preserving its data.

Useful commands:

./scripts/devsuite status                 # show services, ports, and health
./scripts/devsuite logs app               # follow one service
./scripts/devsuite stop                   # stop and preserve the default instance
./scripts/devsuite start --ephemeral      # use a disposable database
./scripts/devsuite start --with-cpa       # include the model gateway; needs Docker
./scripts/devsuite start --production     # reproduce the Caddy deployment locally
./scripts/devsuite --help                 # all commands and options

Production mode serves http://setun.localhost:8080 and http://sandbox.setun.localhost:8080. It builds both applications, runs bun ./server.js behind the repository's Caddy configuration, and uses Caddy's static file server for artifacts. TLS is the only production behavior omitted.

To run the two development servers manually instead:

bun --bun run dev       # application on :5173
bun run dev:sandbox     # sandbox on :5174, in a second terminal

First-time setup

1. Prepare the deployment

You need Docker with Compose, Bun 1.4+, and two DNS names pointing to the host: one for Setun and one for the artifact sandbox.

bun install --frozen-lockfile
cp .env.example .env
cp cpa/config.example.yaml cpa/config.yaml
cp mcp.example.json mcp.json

Fill in .env, then configure the provider account in cpa/config.yaml. Two secrets deserve special care:

  • SETUN_STUDENT_CODE_PEPPER must be high-entropy and permanent. Changing it invalidates every existing student access code.
  • SETUN_CPA_LISTENER_KEY must exactly match the value under api-keys in cpa/config.yaml.

SETUN_APP_ORIGIN and SETUN_SANDBOX_ORIGIN must be full public URLs on different hosts. Their hostname-only counterparts configure Caddy. Leave both educator seed variables blank to use the guided first-run setup. SETUN_APP_ORIGIN is also the app's public origin, which Compose passes to it as ORIGIN (see "Public origin" below).

MCP is optional. If the installation offers no tools, replace the example entry in mcp.json with an empty servers object. Credentials are referenced there by environment-variable name; do not put secret values in the JSON file.

2. Build and start

bun run build:sandbox
docker compose run --rm --no-deps app touch /data/db/setun.sqlite   # new installation only
docker compose up -d --build
docker compose logs app

The sandbox build is explicit because Caddy serves build-sandbox/ directly from the host. The app itself is built into its container image.

A new installation creates its database file deliberately, once. In production Setun refuses to start on a missing database, because that usually means the volume is not mounted, and a fresh database would reopen first-run setup on a configured installation. touch never changes the contents of an existing database.

3. Complete the wizard

On an unconfigured database, Setun writes a one-time setup token to the app log and redirects every route to /setup. Open the application URL, enter the token, and follow the wizard to:

  1. Create the educator account.
  2. Verify the model gateway.
  3. Add the first model alias.
  4. Create a classroom.
  5. Issue the first student access cards.

The token expires after 15 minutes; restarting the app issues a new one. If SETUN_EDUCATOR_SEED_USERNAME and SETUN_EDUCATOR_SEED_PASSWORD are both set, Setun seeds that account at boot and skips the wizard. Updating the seeded credential and restarting remains a supported recovery path for forgotten educator credentials.

Educator credential recovery

The educator login page shows a generic command that an operator with shell access can run against the live database. The browser never performs the reset, and recovery needs neither manual SQL nor an application restart.

From the deployment directory, use the interactive mode to enter a new educator username and a new password. Both password prompts hide terminal input:

docker compose exec app bun /app/recover-educator.js

To have Setun generate a 256-bit password instead, run:

docker compose exec app bun /app/recover-educator.js --generate

The generated password is printed exactly once, on its own terminal line after the database commit. Setun does not copy it to the clipboard or write it to a log or configuration file. Save it before closing the terminal; if it is lost, run recovery again. The new username is always collected by the prompt, so neither credential belongs in shell history or a process listing.

For a source-based installation or manual development server, run the matching package scripts from the repository root:

bun run recover:educator
bun run recover:educator -- --generate

The application must already be running on an existing, migrated database. Compose supplies the container with the same database path and seed environment automatically. For a direct invocation, run from the same configured environment as the application: SETUN_DATABASE_PATH must identify the live database, and the command must see the same SETUN_EDUCATOR_SEED_* values when they are used. Bun loads the repository's .env automatically. The command refuses a missing database, a partial seed pair, a non-interactive terminal, and any password supplied as an argument.

Recovery updates the one educator row and invalidates every live educator session in one immediate SQLite transaction. Pupil sessions and all classroom data, conversations, artifacts, files, and backups are untouched. Exit status 0 means success, 1 an operational failure with no committed change, 2 invalid usage or input, and 130 cancellation.

Boot-time seeds remain backward compatible. A CLI recovery stores only an Argon2id hash identifying the seed pair that it superseded, so restarting with that unchanged pair cannot silently restore the old credentials. Changing either seed value and restarting is treated as an intentional seed-based reset, updates the same educator row, and invalidates educator sessions. Leaving both variables blank keeps the recovered credential under CLI control; the recovery command never edits .env itself.

Student access slips and QR sign-in

The roster can replace one pupil's access code or every active pupil's code and produce A4 access slips for printing or direct PDF download. Reissuing a slip immediately invalidates the previous code and that pupil's signed-in sessions. Plaintext codes exist only in the issuing action response; Setun does not store them, and reloading or leaving the page makes them unrecoverable.

Each QR code contains the configured application origin followed by /login#code=…. The access code is in the URL fragment, so browsers do not send it in HTTP requests or referrers, and the login page removes the fragment from browser history before submitting through the normal sign-in action. PDF files are generated entirely in the browser; no credential-bearing PDF request reaches the server.

An access slip is still a bearer credential: anyone with a copy can sign in as that pseudonymous pupil until the code is rotated, the account is disabled or removed, or normal access policy blocks the sign-in. Store and distribute printed and downloaded copies accordingly.

Configuration notes

  • Provider credentials belong to CLIProxyAPI, not Setun. Its management API, control panel, plugins, and public port are disabled by the supplied configuration.
  • MCP servers are defined in the read-only mcp.json; the educator panel may enable configured servers and tools but cannot add endpoints.
  • Persistent data lives in separate Compose volumes for the database, private storage, backups, provider authentication, and Caddy state.
  • TLS is handled automatically by Caddy for publicly reachable hostnames. For a closed network, use Caddy's internal CA as described in the comments in Caddyfile.
  • Backups contain a consistent SQLite snapshot plus private storage. Fourteen daily snapshots are retained by default; copy the backup volume off-host for disaster recovery.
  • Public origin. Set ORIGIN to the address people open in the browser: scheme, host and port only, for example ORIGIN=http://192.168.1.10:3000. SETUN_APP_ORIGIN means the same, and one of the two is enough; when both are set, ORIGIN wins. Compose takes it from SETUN_APP_ORIGIN in .env. Setun needs it in every deployment:
    • Over plain HTTP, set the http:// address. SvelteKit 3 has no runtime origin setting of its own, so the production entry, bun ./server.js, runs the app on a private socket and tells it this origin on every request.
    • Behind an HTTPS reverse proxy that passes the original Host (the Compose deployment's Caddy), set the https:// address. Setun prints it on access slips, QR codes and the first-run banner.
    • With neither ORIGIN nor SETUN_APP_ORIGIN set, the server stops at startup, before it listens, with Setun needs its public address: set ORIGIN to the address users open, for example ORIGIN=http://192.168.1.10:3000. Only bun run dev falls back to http://localhost:5173.
    • A value with a path, query, fragment or credentials stops the server at startup with ORIGIN must be a bare http(s) origin such as http://192.168.1.10:3000 (no path, query, fragment or credentials). The origin is never taken from the request's Host header.
    • Start production with bun ./server.js, never bun ./build: that skips the origin handling and the process guard.
  • Writes from other origins. Setun refuses any POST, PUT, PATCH or DELETE whose Origin header is not the public origin above, whatever its content type. Browsers send it on their own; a script that calls Setun's API must send Origin: <the public origin> itself.
  • Behind a proxy. Set ADDRESS_HEADER=x-forwarded-for and XFF_DEPTH (the number of proxies in front of Setun, default 1) only when every request passes through them, so the per-address login and rate limits see each pupil's address rather than the proxy's. Compose sets both for its Caddy. PROTOCOL_HEADER and HOST_HEADER are for deployments without ORIGIN; Setun always has an origin, and its front replaces them.
  • Shutdown and request size. SHUTDOWN_TIMEOUT is how many seconds a stopping server lets open requests, such as a pupil's streaming answer, finish: 30 by default, and 7 in the container image, so that it fits inside the 10 seconds docker stop waits. When it has passed, the server exits even if the app still has work in flight, such as a turn waiting on the model. Raise it only together with docker stop -t or Compose's stop_grace_period. BODY_SIZE_LIMIT caps a request body, 2M by default (Compose reads SETUN_BODY_SIZE_LIMIT); restoring an artifact posts the whole project, which may be up to 1 MB.
  • Missing compressed files. The build ships a .br and a .gz copy of every static file, and the server sends one when the browser accepts it. If one of those copies goes missing on disk while the server runs (for example, a new build copied over a running one), the front asks the app for the plain file instead, so the page keeps working.

Development

Every build has two outputs:

  • build/ — the application, built by @sveltejs/adapter-bun and started by bun ./server.js
  • build-sandbox/ — the static artifact host, runtimes, and compilers

Always use bun run build; running only the SvelteKit build leaves the artifact panel without the files it needs. The development suite keeps each named instance under .devsuite/instances/ with its own database, logs, and production build outputs.

Quality gates

Run the application checks from the package scripts and Python checks with bash scripts/check-python.sh. Run repository hygiene checks locally with prek; prek run --all-files --hook-stage manual is the set CI runs. CI also runs the component and Playwright suites below, bun run build && bun run smoke, a cold production start, and bun run smoke:container, which starts the Compose deployment with dummy settings and checks it through Caddy (it needs Docker and the build).

bun run check         # Svelte and TypeScript correctness
bunx biome ci         # formatting and linting
bun test              # server logic and rune modules
bun run test:component # Chromium components and Vite server tests
bunx playwright test  # end-to-end flows
bun run check:python  # locked Ruff, format, strict types, syntax, dev-suite tests

Install Chromium once with bunx playwright install chromium if it is not already available.

Test filenames select their runner:

Scope Filename Runner
Server logic and .svelte.ts rune modules *.test.ts Bun
Svelte components *.svelte.spec.ts Vitest Browser Mode
Other tests needing Vite resolution *.spec.ts Vitest server project
Real-server flows *.e2e.ts Playwright

Project conventions

  • Add every user-facing message to both messages/en.json and messages/da.json.
  • Styling uses UnoCSS, not Tailwind. Add shadcn-svelte components with bunx shadcn-svelte add <component> --skip-preflight; never run init.
  • Use bun run check as the type authority for Svelte templates. Biome uses experimental markup support with the compatibility overrides documented below; never apply blanket unsafe fixes.
  • The design baseline is tweakcn's clean-slate theme, ported to bare oklch components in uno.config.ts. A <style> block reading one of those variables must wrap it — oklch(var(--muted)) — because the preset's utilities supply the wrapper and a bare var() silently resolves to transparent. Dark mode is <html class="dark">; check both themes.
  • Install prek before committing. Hooks enforce Conventional Commits, protect main, and scan for secrets.

License

Setun is licensed under the GNU Affero General Public License v3.0.

Biome configuration

biome.json uses the pinned Biome version, Git ignore rules, recommended lint and import-organizing rules, and the existing two-space, 100-column, double-quote style. Its maintained scope remains src/, sandbox/, and root JavaScript, TypeScript and JSON files. Generated Paraglide files and build output are excluded. Run bun run lint to check or bun run format to apply formatting and safe fixes.

Experimental HTML support enables Svelte markup checks and formatting; every component is formatted. Narrow lint compatibility overrides preserve:

  • Custom tree, radio-button, separator and grouped controls with intentional ARIA roles.
  • The elicitation label whose native control is inside a Svelte conditional.
  • Component scope props, which are unrelated to HTML table-header scope, and the language placeholder in src/app.html.

Keep bun run check as the framework/type check. Do not apply blanket unsafe fixes to components; re-evaluate these overrides when upgrading Biome.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages