diff --git a/README.md b/README.md
index df48bd1..1476b16 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,31 @@
# Getting started
-Before starting, make sure you have Python 3.11 or newer and Node.js 20 available.
+Before starting, make sure you have [mise](https://mise.jdx.dev) installed. It manages Python, Node, uv and prek.
```bash
+mise install
+prek install
uv run manage.py migrate
cp .env-default .env
uv run manage.py seed_demo_data
# Optional: create an admin login
uv run manage.py createsuperuser
-
-# Install frontend packages once
-cd frontend && npm install
```
Start the backend and frontend in separate terminals from the project root:
```bash
-uv run bin/run
+mise run server
```
```bash
-uv run bin/run-frontend
+mise run frontend-run
```
Vite prints the frontend URL when it starts (normally ; if that port is busy, it selects another). Django runs at ; open the admin directly at or through the frontend dev proxy at the printed Vite URL followed by `/admin/`. The GraphQL API is at ; the frontend uses Vite's proxy for development API requests.
-To test the frontend against a different backend, set `VITE_DEV_API_PROXY` when starting Vite, for example `VITE_DEV_API_PROXY=https://bbs.neuf.no uv run bin/run-frontend`. This affects the development proxy only; use caution with a live backend, since app changes and submissions may modify production data.
+To test the frontend against a different backend, set `VITE_DEV_API_PROXY` when starting Vite, for example `VITE_DEV_API_PROXY=https://bbs.neuf.no mise run frontend-run`. This affects the development proxy only; use caution with a live backend, since app changes and submissions may modify production data.
To create the admin during data seeding instead, set `BBS_SEED_ADMIN_PASSWORD` for that command; the command only creates a local `admin` superuser if one does not already exist.
diff --git a/bin/generate_schema b/bin/generate_schema
deleted file mode 100755
index 8a2533f..0000000
--- a/bin/generate_schema
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-
-python manage.py export_schema
-cd frontend && npm run generate
diff --git a/bin/pre-commit b/bin/pre-commit
deleted file mode 100755
index 19d5b62..0000000
--- a/bin/pre-commit
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-uvx pre-commit run --all-files
diff --git a/bin/run b/bin/run
deleted file mode 100755
index a7b6db9..0000000
--- a/bin/run
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-exec python manage.py runserver 127.0.0.1:3000
\ No newline at end of file
diff --git a/bin/run-frontend b/bin/run-frontend
deleted file mode 100755
index 098589a..0000000
--- a/bin/run-frontend
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-cd frontend && npm run start -- --host 127.0.0.1
diff --git a/bin/test b/bin/test
deleted file mode 100755
index cd58336..0000000
--- a/bin/test
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-pytest -v
\ No newline at end of file
diff --git a/mise.toml b/mise.toml
index 68ec4c6..f56ea56 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,6 +1,40 @@
[tools]
+node = "20"
+prek = "latest"
python = "3.11"
uv = "latest"
+[tasks.server]
+run = "uv run manage.py runserver 127.0.0.1:3000"
+
+[tasks.frontend-install]
+run = "npm install"
+dir = "frontend"
+sources = ["package.json", "package-lock.json"]
+outputs = ["node_modules/.package-lock.json"]
+
+[tasks.frontend-run]
+depends = ["frontend-install"]
+run = "npm run start"
+dir = "frontend"
+
+[tasks.frontend-build]
+depends = ["frontend-install"]
+run = "npm run build"
+dir = "frontend"
+
+[tasks.generate-schema]
+depends = ["frontend-install"]
+run = "uv run manage.py export_schema && cd frontend && npm run generate"
+
+[tasks.pre-commit]
+run = "prek run --all-files"
+
[tasks.test]
run = "uv run pytest --cov"
+
+[tasks.lint]
+run = "uv run ruff check ."
+
+[tasks.fmt]
+run = "uv run ruff format ."