Project structure¶
An uproot project is a Python package with a specific layout. Here is what a typical project looks like.
Directory layout¶
my_project/
├── main.py # Entry point and configuration
├── pyproject.toml # Python dependencies
├── requirements.txt # Alternative dependency file
├── .env # Local environment settings
├── Procfile # For cloud deployment (Heroku, Railway)
├── uproot_license.txt # uproot’s LGPL license
├── _static/ # Static files shared by all apps (optional)
├── my_app/
│ ├── __init__.py # App logic: pages, fields, callbacks
│ ├── Welcome.html # Template for Welcome page
│ ├── Decision.html # Template for Decision page
│ ├── Results.html # Template for Results page
│ ├── simulate.js # Automated page interactions for testing (optional)
│ └── _static/ # App-specific static files (optional)
│ └── diagram.png
└── another_app/
├── __init__.py
└── ...
The main.py file¶
The entry point configures your experiment and starts the server:
import uproot.deployment as upd
from uproot.cli import cli
from uproot.server import load_config, uproot_server
upd.project_metadata(created="1970-01-01", uproot="*.*.*")
load_config(uproot_server, config="my_experiment", apps=["my_app"])
upd.ADMINS["admin"] = upd.auto_login() # Secret login link, or password from the environment
upd.LANGUAGE = "en" # Built-in: "de", "en", "es", "fr", "ja"
if __name__ == "__main__":
cli()
Configs¶
Each load_config call registers a config: a named experiment configuration that specifies which apps to run and in what order:
# A single-app config
load_config(uproot_server, config="survey", apps=["survey"])
# A multi-app config: participants go through both apps in sequence
load_config(uproot_server, config="full_experiment", apps=["instructions", "game", "survey"])
When you create a session in the admin, you select a config. The session runs all listed apps in order.
During development, the fastest way to try a config is uproot start: uv run uproot start my_experiment starts the server and prints a participant link for that config, with no admin steps needed.
You can optionally pass a settings dictionary to load_config that provides default session settings:
load_config(
uproot_server,
config="my_experiment",
apps=["my_app"],
settings={"n_rounds": 5, "show_feedback": True},
)
In addition, uproot registers every app on its own, under the app’s name with a tilde (such as ~my_app). The admin lists these under “Apps.” They are handy for testing one app in isolation, but they have no settings. If your app reads session.settings, give defaults, as in session.settings.get("n_rounds", 5), or fail with a clear message in new_session when a required setting is missing.
Admin accounts¶
Keep the default setup to sign in with a secret login link or a password:
When UPROOT_ADMIN_PASSWORD is unset or empty and admin is the only admin account, uproot prints a login link containing a cryptographically random secret token. You can use this locally or in production; keep the link private and use HTTPS on public servers. For password login, set UPROOT_ADMIN_PASSWORD through your hosting provider’s secret settings or a .env file kept out of Git. See Admin authentication for setup instructions and multiple accounts.
API keys¶
UPROOT_API_KEY is read by the uproot api client. It does not register a
server token by itself; add the token to upd.API_KEYS in the project.
Default rooms¶
from uproot.rooms import room
upd.DEFAULT_ROOMS.append(
room("my_room", config="my_experiment", labels=["A", "B", "C"])
)
Rooms defined this way are created automatically when the server starts.
App module¶
Each app is a Python package (a directory with __init__.py). The __init__.py defines the experiment logic.
Required¶
page_order: list of page classes (and SmoothOperators) that define the participant flow:
Optional module-level attributes¶
| Attribute | Purpose |
|---|---|
DESCRIPTION |
Human-readable description shown in admin |
SUGGESTED_MULTIPLE |
Hint for session creation (e.g., 2 for pair experiments) |
LANDING_PAGE |
If True, inserts a landing page before the app’s pages. Override it with LandingPage.html in the app directory, or add extra text with LandingPageInfo.html |
C |
Constants class, available in templates as C. Set C.__export__ to a list of names (or ...) to copy those constants into JavaScript as window.C |
Optional callbacks¶
| Callback | When it runs |
|---|---|
new_session(session) |
Once when session initializes |
new_player(player) |
Once per player when they join |
restart() |
On server restart (can be async) |
digest(session) |
Returns data for the admin digest view (pair with AdminDigest.html) |
pipeline(session) |
Admin-runnable job; return a list of dicts for a downloadable table. May take optional data= |
language(player) |
Returns ISO 639 language code for the player |
api(request, session) |
Authenticated HTTP endpoint at /api/{app}/{sname}/ |
api2(request, session, player=None) |
Participant HTTP endpoint at /api2/{app}/{sname}/; public by default, with optional participant authentication |
See Storing and accessing data for details on new_session and new_player. See App HTTP APIs for the advanced api2 authentication behavior.
Page classes¶
Pages are defined as classes that inherit from Page, NoshowPage, GroupCreatingWait, or SynchronizingWait:
class Welcome(Page):
pass
class Calculate(NoshowPage):
@classmethod
def after_always_once(page, player):
player.score = player.correct * 10
See Pages and templates for details.
Templates¶
Each displayed page needs a corresponding HTML or Markdown template in the same app directory. By default, uproot first looks for an .html file matching the class name, then for .md:
my_app/
├── __init__.py # class Welcome(Page)
└── Welcome.html # Template for Welcome (or Welcome.md)
HTML templates extend a base layout:
{% extends "Base.html" %}
{% block title %}Welcome{% endblock title %}
{% block main %}
<h1>Welcome to the experiment</h1>
{% endblock main %}
Using PlayerContext¶
Define a Context class for computed values accessible across all templates:
class Context(PlayerContext):
@property
def total_earnings(self):
return self.player.payoff * C.EXCHANGE_RATE
Available in templates as player.context.total_earnings. See The PlayerContext class for Python access and app lifecycle details.
Database¶
uproot uses SQLite by default. The database file uproot.sqlite3 is created automatically in your project directory when the server starts. No configuration needed.
SQLite works well in production too; uproot is optimized for it. PostgreSQL is available as an alternative but is never required. To use it, add PostgreSQL support to your project with uv add 'uproot-science[pg]<1'. See Deployment for details.
Why uproot loves SQLite¶
SQLite is fast, exceptionally well tested, and stable. It needs no separate database server to install or manage, so you can run a real experiment with the same simple setup you used to build it. It just works, even if you have massive experiments that are highly simultaneous. For a look at the care behind that reliability, watch SQLite creator Dr D. Richard Hipp’s talk, “Reliability Lessons From SQLite”.
Environment variables¶
| Variable | Default | Purpose |
|---|---|---|
UPROOT_DATABASE |
sqlite3 |
Database driver (sqlite3, memory, postgresql) |
UPROOT_SQLITE3 |
uproot.sqlite3 |
SQLite file path |
UPROOT_POSTGRESQL |
— | PostgreSQL connection URL |
UPROOT_ORIGIN |
— | Public server URL |
UPROOT_SUBDIRECTORY |
— | Subdirectory prefix for all routes |
UPROOT_API_KEY |
— | Bearer token used by the uproot api client |
UPROOT_ADMIN_PASSWORD |
— | Password used by upd.auto_login(); unset or empty enables auto-login for a sole admin account |
UPROOT_ALLOW_ENTER |
off | If 1/true/yes/on, the Enter key submits participant forms |
Run uproot deployment to see the current values.
What’s next?¶
- Pages and templates: How pages work.
- Collecting data with forms: Available field types.
- The admin interface: Managing your experiments.