The WebUI is a SvelteKit web application for browsing and editing the Open Filament Database. It runs in two modes — local (direct filesystem access) and cloud (remote API with change-tracking) — so contributors can edit data from a cloned repo or from the hosted instance.
- Hosted Editor
- Getting Started
- Application Modes
- Pages
- Features
- Architecture
- API Reference
- Configuration
Most contributors don't need to install anything. The hosted instance runs the WebUI in cloud mode and is available at:
👉 http://openfilamentdatabase.org/
Sign in with your GitHub account and any edits you make are tracked in your browser and submitted as a pull request when you're ready. Anonymous submissions are also supported when enabled — see Cloud Mode and GitHub Integration for details.
The hosted URL is recorded under
[project.urls].WebUIinpyproject.tomland is the single source of truth — if it ever moves, that entry is updated first.
The rest of this document covers running the same editor locally, which is useful for offline work, contributing to the WebUI itself, or running the Python validator against a local clone.
The easiest way to launch the WebUI locally is with the OFD wrapper script, which handles Python, Node.js, and dependency setup automatically.
Linux / macOS:
./ofd.sh webuiWindows:
ofd.bat webuiOn first run the wrapper will:
- Check for Python 3.10+ and Node.js (offering to install if missing)
- Create a Python virtual environment
- Install all required dependencies
- Start the SvelteKit dev server
Then open http://localhost:5173 and start editing.
./ofd.sh webui # Start on default port (5173)
./ofd.sh webui --port 3000 # Start on a custom port
./ofd.sh webui --host 0.0.0.0 # Bind to all interfaces (e.g. for LAN access)
./ofd.sh webui --open # Open browser automatically
./ofd.sh webui --install # Force reinstall npm dependenciesThe same flags work with ofd.bat on Windows.
-
Install Git, Python 3.10+, and Node.js
-
Enter the WebUI directory and install dependencies:
cd webui npm ci -
Start the development server:
npm run dev
-
Navigate to http://localhost:5173
The WebUI supports two modes, controlled by the PUBLIC_APP_MODE environment variable.
- Reads and writes JSON files directly on the filesystem under
data/andstores/. - Save, sort, and validate operations invoke Python subprocesses.
- No authentication required — all operations are immediate.
- Intended for contributors who have cloned the repository locally.
- Fetches data from an external API (configured via
PUBLIC_API_BASE_URL). - Edits are tracked in the browser's
localStorageas pending changes. - Submitting changes creates a GitHub pull request (authenticated via OAuth) or an anonymous submission (via a bot account).
- In-process validation using AJV (no Python dependency).
- Designed for the hosted instance where users cannot write to the filesystem.
Entry point showing cards for Brands, Stores, FAQ, and API (if a base URL is configured). Each card displays a count and links to the relevant section.
Grid of all brands with logos and names. Includes a Create Brand button to add new brands. Change indicators show which brands have pending edits.
Displays brand properties (name, origin, website, logo) with edit and delete controls. Lists all materials under the brand with links to drill deeper.
Shows the material type (PLA, ABS, PETG, etc.) and its properties. Lists filaments under this material.
Displays filament properties including slicer settings (temperatures, speeds, retraction). Lists colour variants.
Leaf level of the hierarchy. Shows variant-specific data: colour hex, name, sizes (weight, spool dimensions), purchase links with store references, certifications, and discontinued status.
Grid of all stores with logos. Create / edit / delete support with change tracking.
Store properties: name, storefront URL, shipping regions.
Categorised FAQ page covering: General, Local Mode, Cloud Mode, IDs, and Change Tracking. Shows the current mode indicator.
The database is organised in layers:
Brand (e.g. Bambu Lab, eSUN, SUNLU)
└── Material (e.g. PLA, ABS, PETG)
└── Filament (e.g. Sparkly, Basic)
└── Variant (e.g. Black, Rainbow)
└── Sizes & Purchase Links
Every entity above (including each spool under Sizes) has a stable, slug-independent canonical uuid. The WebUI never asks you to enter it and never generates one: new entries are submitted with an empty uuid, and CI assigns a permanent UUID when your pull request is merged. Editing an existing entry preserves its UUID unchanged. See Canonical UUIDs for details.
In cloud mode, all creates, updates, and deletes are recorded in localStorage:
- The Changes Menu (top-right badge) shows the number of pending changes.
- Individual changes can be undone or inspected.
- All changes can be exported as JSON or cleared.
- Changes persist across page reloads until submitted or cleared.
In local mode, changes are saved directly to disk — no tracking needed.
- Cloud mode: In-process AJV validation against JSON schemas. Validates entity structure, folder naming, image formats, GTIN checksums, and store-ID cross-references.
- Local mode: Invokes the Python
ofd validatescript as a subprocess with real-time SSE progress streaming.
Validation results show errors (red) and warnings (yellow) with paths linking to the offending entities.
Local mode only. Runs ofd script style_data to alphabetically sort JSON keys for consistency. Progress is streamed via SSE.
Light, dark, and system-follow themes via the header dropdown. Preference is stored in localStorage.
- Authenticated PRs: Users sign in via GitHub OAuth, and the editor forks the upstream repo, creates a branch, builds a commit from changes, and opens a pull request — all through the GitHub API.
- Anonymous PRs: When enabled, users can submit changes without a GitHub account. A bot account creates the PR on their behalf. Submissions are tracked by UUID, and lifecycle events (submitted, merged, closed) fire webhooks.
Brands and stores support logo uploads with client-side validation:
- Allowed formats: PNG, JPG, JPEG, SVG, GIF, WebP
- Maximum file size: 5 MB
- Logos are stored alongside the entity JSON in the data directory.
webui/
├── src/
│ ├── routes/ # SvelteKit file-based routing
│ │ ├── +layout.svelte # Global layout (header, nav, theme, changes menu)
│ │ ├── +page.svelte # Home page
│ │ ├── brands/ # Brand hierarchy pages
│ │ ├── stores/ # Store pages
│ │ ├── faq/ # FAQ page
│ │ └── api/ # API endpoints (see API Reference)
│ ├── lib/
│ │ ├── components/ # Svelte components
│ │ │ ├── layout/ # ChangesMenu, DataDisplay
│ │ │ ├── forms/ # Entity form components
│ │ │ ├── form-fields/ # Reusable field components
│ │ │ ├── entity/ # EntityCard, EntityDetails, Logo
│ │ │ ├── ui/ # Button, Modal, LoadingSpinner, etc.
│ │ │ └── icons/ # SVG icon components
│ │ ├── services/ # Client-side services
│ │ │ ├── database.ts # Main DB abstraction (local/cloud)
│ │ │ ├── entityService.ts
│ │ │ ├── schemaService.ts
│ │ │ └── imageStorage.ts
│ │ ├── server/ # Server-side modules
│ │ │ ├── auth.ts # GitHub OAuth token management
│ │ │ ├── cloudValidator.ts # In-process AJV validation
│ │ │ ├── cloudProxy.ts # Cloud/local routing abstraction
│ │ │ ├── entityConfig.ts # Entity type configurations
│ │ │ ├── entityCrud.ts # Factory CRUD handler generators
│ │ │ ├── github.ts # GitHub REST API client
│ │ │ ├── prBuilder.ts # Shared PR-building logic
│ │ │ ├── anonBot.ts # Anonymous submission bot
│ │ │ ├── rateLimit.ts # IP-based sliding window rate limiter
│ │ │ ├── submissionStore.ts # UUID-to-PR tracking (in-memory + file)
│ │ │ ├── webhooks.ts # HMAC-signed webhook dispatch
│ │ │ ├── jobManager.ts # Async job tracking
│ │ │ ├── saveUtils.ts # Filesystem save utilities
│ │ │ ├── logoHandler.ts # Logo file management
│ │ │ ├── imageValidation.ts # Image format/size validation
│ │ │ └── pathValidation.ts # Path traversal prevention
│ │ ├── stores/ # Svelte stores
│ │ │ ├── changes.ts # Change tracking state
│ │ │ ├── auth.ts # Authentication state
│ │ │ ├── environment.ts # Mode detection
│ │ │ └── theme.ts # Theme preference
│ │ ├── types/
│ │ │ └── database.ts # TypeScript type definitions
│ │ └── utils/ # Shared utilities
│ ├── hooks.server.ts # Server startup & env validation
│ └── app.html # HTML shell
├── svelte.config.js # SvelteKit config (Node adapter for Docker)
├── vite.config.ts # Vite + Tailwind CSS v4
├── .env.example # Environment variable reference
└── package.json
- SvelteKit (Svelte 5) with TypeScript
- Tailwind CSS v4 for styling
- AJV for JSON Schema validation (cloud mode)
- SJSF for dynamic form generation from schemas
- Node.js adapter for Docker deployment, auto adapter for development
All endpoints are served under /api/. In cloud mode, entity-read endpoints proxy to the external API. In local mode, they read from the filesystem. Write endpoints (PUT, DELETE, POST for creates) operate on the local filesystem only — in cloud mode, changes are tracked client-side and submitted as pull requests.
Initiates the GitHub OAuth flow. Generates a random state token stored in an httpOnly cookie (10-minute TTL) and redirects to GitHub's authorisation endpoint.
- Scopes requested:
public_repo workflow - Response: 302 redirect to GitHub
OAuth callback handler. Validates the state parameter against the cookie, exchanges the authorisation code for an access token, and stores the token in an httpOnly cookie (ofd_gh_token, 30-day TTL).
- Query params:
code,state - Response: 302 redirect to
/withauth_success=trueorauth_error=<reason>
Clears the GitHub token cookie.
- Response:
{ "success": true }
Returns the current authentication status. If a token is present, fetches the GitHub user profile to verify it is still valid.
-
Response (authenticated):
{ "authenticated": true, "user": { "login": "...", "name": "...", "avatar_url": "..." } } -
Response (not authenticated):
{ "authenticated": false }
List all brands.
- Response: Array of brand objects.
Get a single brand by ID.
- Response: Brand object.
Update a brand. Local mode only.
- Request body: Brand JSON.
- Response:
{ "success": true }
Delete a brand and all its children. Local mode only.
- Response:
{ "success": true }
Upload a brand logo.
- Request body:
{ "brandId": "...", "imageData": "data:image/png;base64,..." } - Response:
{ "success": true, "path": "..." }
Delete a brand logo.
- Request body:
{ "brandId": "..." } - Response:
{ "success": true }
Serve a brand's logo image file.
List all materials for a brand.
Create a new material. Local mode only.
- Request body:
{ "material": "PLA", ... } - Response:
{ "success": true, "material": { ... } }(201) - Note: The
materialfield is used as the directory name (uppercased).
Get a single material.
Update a material. Local mode only.
Delete a material and all its children. Local mode only.
List all filaments for a material.
Create a new filament. Local mode only.
- Request body:
{ "name": "Basic", ... } - Response:
{ "success": true, "filament": { ... } }(201)
Get a single filament.
Update a filament. Local mode only.
Delete a filament and all its children. Local mode only.
List all variants for a filament. Sizes are merged from a separate sizes.json file.
Create a new variant. Local mode only.
- Request body:
{ "name": "Black", "color_hex": "#000000", "sizes": [...], ... } - Response:
{ "success": true, "variant": { ... } }(201) - Note:
sizesis extracted and written to a separatesizes.jsonfile.
Get a single variant with sizes merged in.
Update a variant. Local mode only.
Delete a variant. Local mode only.
List all stores.
Get a single store.
Update a store. Local mode only.
Delete a store. Local mode only.
Upload a store logo.
- Request body:
{ "storeId": "...", "imageData": "data:image/png;base64,..." }
Delete a store logo.
- Request body:
{ "storeId": "..." }
Serve a store's logo image file.
Returns the JSON schema for brand entities.
Returns the JSON schema for material entities.
Returns the JSON schema for filament entities.
Returns the JSON schema for variant entities.
Returns the JSON schema for store entities.
Returns the list of valid material types.
Start a validation job.
-
Request body:
{ "type": "full | json_files | logo_files | folder_names | store_ids", "changes": [...], "images": { ... }, "sessionId": "uuid" } -
Response:
{ "jobId": "...", "sseUrl": "..." } -
Local mode: Spawns Python subprocess (
python3 -m ofd validate). -
Cloud mode: Runs in-process AJV validation.
sessionIdis required. -
Error 409: A job is already running.
Check whether a validation job is currently running.
- Query params:
sessionId(required in cloud mode) - Response:
{ "running": true|false, "jobId": "...", "status": "..." }
SSE endpoint streaming validation progress events.
- Content-Type:
text/event-stream - Events: Progress updates, error reports, completion signal.
Retrieve the final validation result (fallback if SSE missed the completion event).
- Response:
{ "status": "...", "result": { ... } }
Start a data sorting job. Local mode only.
- Request body:
{ "dryRun": false, "runValidation": false } - Response:
{ "jobId": "...", "sseUrl": "..." } - Error 403: Not available in cloud mode.
- Error 409: Already running.
SSE endpoint streaming sort progress events. Local mode only.
Batch-save all pending changes to the filesystem. Local mode only.
-
Request body:
{ "changes": [ { "entity": { "path": "brands/prusament" }, "operation": "create | update | delete", "data": { ... } } ], "images": { "img-id": { "data": "base64...", "filename": "logo.png", "entityPath": "brands/prusament" } } } -
Response (success):
{ "success": true, "message": "...", "results": [...], "imageResults": [...], "styleData": {...}, "validation": {...} } -
Response (partial): Same shape, HTTP 207.
-
Error 403: Not available in cloud mode.
-
Behaviour: Processes deletes first, then creates/updates. Writes logo images. Runs
style_datafor key sorting. Runs validation.
Create a GitHub pull request from pending changes. Requires GitHub OAuth authentication.
-
Request body:
{ "changes": [...], "images": { ... }, "title": "Optional PR title", "description": "Optional PR description" } -
Response:
{ "success": true, "prUrl": "...", "prNumber": 123 } -
Error 401: Not authenticated.
-
Behaviour: Forks the upstream repo (idempotent), creates a timestamped branch, builds a Git tree from changes, creates a commit, and opens a PR.
Submit changes anonymously via a bot account. Only available when the anonymous bot feature is enabled.
-
Request body:
{ "changes": [...], "images": { ... }, "title": "Optional", "description": "Optional", "email": "Optional, never stored by webui" } -
Response:
{ "success": true, "uuid": "...", "prUrl": "...", "prNumber": 123 } -
Error 422: Validation errors found — includes
validationobject. -
Error 429: Rate limited — includes
Retry-Afterheader. -
Rate limit: Per-IP sliding window, default 5 submissions per hour.
-
Privacy: Email is only forwarded in the outgoing webhook and is never persisted.
Check the status of an anonymous submission.
-
Response:
{ "uuid": "...", "status": "open | merged | closed", "prUrl": "...", "prNumber": 123, "createdAt": "2025-01-15T10:30:00Z" } -
Error 404: Feature disabled or submission not found.
Receives incoming GitHub webhook events for PR lifecycle tracking.
- Headers:
x-hub-signature-256(HMAC-SHA256),x-github-event - Supported events:
pull_requestwith actionclosed - Behaviour: Verifies the signature, extracts the submission UUID from the PR body, updates the submission status to
mergedorclosed, and fires outgoing webhooks. - Error 401: Invalid signature.
All configuration is via environment variables. See .env.example for the full reference.
| Variable | Required | Default | Description |
|---|---|---|---|
PUBLIC_APP_MODE |
Yes | — | local or cloud |
PUBLIC_API_BASE_URL |
Cloud mode | https://api.openfilamentdatabase.org |
External API base URL |
| Variable | Default | Description |
|---|---|---|
PUBLIC_CACHE_ENABLED |
true |
Enable API response caching |
PUBLIC_CACHE_TTL_DEFAULT |
300 |
Default TTL in seconds |
PUBLIC_CACHE_TTL_SCHEMAS |
86400 |
Schema endpoint TTL (1 day) |
PUBLIC_CACHE_TTL_INDEX |
300 |
List endpoint TTL |
PUBLIC_CACHE_TTL_ENTITY |
900 |
Entity endpoint TTL |
| Variable | Required | Description |
|---|---|---|
PUBLIC_GITHUB_CLIENT_ID |
For PRs | GitHub OAuth App client ID |
GITHUB_CLIENT_SECRET |
For PRs | GitHub OAuth App secret |
GITHUB_REDIRECT_URI |
For PRs | OAuth callback URL |
GITHUB_UPSTREAM_OWNER |
For PRs | Upstream repo owner (default: OpenFilamentCollective) |
GITHUB_UPSTREAM_REPO |
For PRs | Upstream repo name (default: open-filament-database) |
| Variable | Required | Default | Description |
|---|---|---|---|
PUBLIC_ANON_BOT_ENABLED |
No | false |
Enable anonymous submissions (client flag) |
ANON_BOT_ENABLED |
No | false |
Enable anonymous submissions (server flag) |
ANON_BOT_GITHUB_PAT |
When enabled | — | Bot account personal access token (public_repo scope) |
ANON_BOT_GITHUB_USERNAME |
When enabled | — | Bot GitHub username |
| Variable | Required | Description |
|---|---|---|
WEBHOOK_URL_SUBMITTED |
No | URL called when an anonymous PR is created |
WEBHOOK_URL_MERGED |
No | URL called when a PR is merged |
WEBHOOK_URL_CLOSED |
No | URL called when a PR is closed without merge |
WEBHOOK_SECRET |
No | HMAC-SHA256 secret for signing outgoing webhook payloads |
GITHUB_WEBHOOK_SECRET |
When bot enabled | Secret for validating incoming GitHub webhook signatures |
| Variable | Default | Description |
|---|---|---|
ANON_RATE_LIMIT_PER_HOUR |
5 |
Maximum anonymous submissions per IP per hour |