Viron is your foundational spatial simulation service β the bedrock on which worlds are built.
It manages environments, grids, locations, and entities through a clean REST API so you can skip the boilerplate and focus on fun, emergent gameplay.
It serves as a reusable backend component for simulation-based games, AI experiments, and virtual world applications.
Viron has no UI of its own; this is a 150-tick random walk driven through a real local Viron's REST API, recorded tick by tick by Viron USD and rendered in Blender. GIF reproduced from that repository (docs/images/replay.gif, commit 8a6bea2), where the recording and render scripts live.
Every game and simulation needs a shared understanding of where things are and how they relate.
Without Viron, developers waste weeks reinventing grid systems, spatial queries, and entity placement logic for each new project.
With Viron:
- Creating an entire environment takes seconds.
- Populating it with entities is a single API call.
- Debugging is instant and visual.
- Multiplayer is a breeze because the world is already consistent for every client.
You sit down with your morning coffee.
In five minutes, youβve:
- Spawned an entire planet via
POST /api/v1/environments. - Populated it with hundreds of plants and creatures with one request.
- Watched the simulation come alive as agents move, interact, and adapt.
- Debugged a gameplay issue in seconds by querying exactly whatβs in a problem area.
Instead of building coordinate math and entity managers from scratch, youβre free to design mechanics, test wild ideas, and ship faster.
Viron abstracts away low-level spatial data management so client applications can focus on simulation logic, rendering, and game mechanics.
Core responsibilities:
- Managing hierarchical spatial structures (environments β grids β locations).
- Tracking entity placement and movement.
- Providing clean, testable REST APIs.
- Offering debug tools for rapid development and testing.
The MVP implements the endpoints defined in docs/openapi/viron-api.json and documented in docs/MVP.md.
Environment Management
- Create, retrieve, rename, and delete environments.
- Create grids as squares (
gridSize) or with independent dimensions (numRows/numColumns). - Query environments by ID, name, or contained entity.
Grid Management
- Retrieve grids by ID or environment.
- Find the grid containing a specific entity.
- Rename a grid.
Location Management
- Retrieve locations by ID, grid, or environment.
- Place an entity at a location, move a placed entity to an adjacent location, and remove it from a location or from wherever it currently stands.
- Retrieve the entities a location holds, report whether it holds any at all, and find the locations in a grid that hold none.
- Retrieve the locations adjacent to a location within its grid.
- Retrieve where every placed entity stands in one consistent read, across all environments or within one.
Entity Management
- Create, retrieve, rename, and delete entities.
- Query entities by environment, grid, or location, and list the entities that are not placed anywhere.
Debug Utilities
- Generate sample environments, grids, locations, and entities.
- Quickly create a world and place an entity for testing.
- Disabled by default β set
VIRON_DEBUG_ENABLED=true(propertyviron.debug.enabled) to register/api/v1/debug/**. Left off, those endpoints are not mapped at all, so debug tooling does not ship to production. - The debug endpoints build their data by calling this API back over HTTP (at
service.vironHost:service.vironPort), so they need a bearer token of their own: setVIRON_CLIENT_AUTH_TOKEN(propertyservice.authToken) to a JWT issued by UserAuth. Without it every call they make is answered401, and the debug request fails with500.compose.ymlpasses it through to the container from.env(seesample.env).
For detailed endpoint definitions and request/response formats, see
docs/MVP.mdanddocs/openapi/viron-api.json.
- Java 21
- Spring Boot 3
- Lombok
- MapStruct (model β DTO mapping)
- Spring Security (OAuth2 resource server / JWT)
- PostgreSQL (persistence layer), pooled with HikariCP
- Maven (build tool)
- Docker + Docker Compose (deployment)
- Swagger/OpenAPI (API documentation)
- JaCoCo (test coverage)
- Planned: Flyway (future migrations); schema currently comes from
db-scripts/setup/, with changes to an already-created schema kept as hand-run scripts indb-scripts/migrations/
viron/
βββ src/main/java/preponderous/viron/
β βββ config/ # Spring configuration (security, database, OpenAPI)
β βββ controllers/ # REST controllers (Environment, Grid, Location, Entity, Debug)
β βββ database/ # JDBC access helpers
β βββ dto/ # Data Transfer Objects for API requests/responses to keep internal models private
β βββ exceptions/ # Exception types and the global exception handler
β βββ factories/ # Creation logic for environments and entities
β βββ mappers/ # MapStruct mappers between models and DTOs
β βββ models/ # Internal domain models
β βββ repositories/ # Data access layer
β βββ services/ # Java HTTP client of this API (counterpart of the Python client), used by the debug endpoints
β βββ trace/ # Usage reporting (vendored trace client + the startup event)
βββ src/main/python/ # Python client SDK
βββ src/test/java/... # Unit and integration tests
βββ src/test/python/... # Python client SDK tests (pytest)
βββ db-scripts/ # SQL schema setup scripts, and migrations for existing databases
βββ docs/
β βββ MVP.md # Implementation checklist for MVP
β βββ PLANNING.md # MVP issue plan, grouped into milestones
β βββ REBUILD_PLAN.md # Process and conventions for the ground-up rebuild
β βββ diagrams/ # Class-usage diagram (draw.io source and rendered PNG)
β βββ openapi/
β βββ viron-api.json # API specification
βββ postman/ # Postman collection generated from the API specification
βββ pom.xml # Maven configuration
βββ README.md # This file
- Java 21
- Maven 3.9+
- Docker & Docker Compose
mvn clean install
Copy sample.env to .env and review the values before starting anything.
JWT_SECRET is required β it has no default, so the service will not start until it is set.
It must match the secret used by the UserAuth service that issues the tokens, and for HS256
it must be at least 32 bytes.
On start-up the service sends one startup event to the trace usage-tracking service at
https://trace.danielstephenson.dev: the program name (viron), its version, and the tag
service=true. Nothing is sent per request, and nothing about users, hosts, addresses or data
is ever included. The event goes out on a background thread and is dropped if the trace server
is down or slow, so it can never delay start-up or a request.
Every event also carries a random installation ID as the tag install, so the number of
running deployments can be counted rather than events. It is the value of TRACE_INSTALL_ID
when that is set (to pin one ID per deployment), and otherwise a random UUID written the first
time reporting runs to <user data dir>/viron/trace-install-id ($XDG_DATA_HOME or
~/.local/share on Linux, ~/Library/Application Support on macOS, %APPDATA% on Windows)
and reused after that. In a container that directory is usually not on a volume, so a recreated
container counts as a new installation unless TRACE_INSTALL_ID is set. The ID identifies no
person, account, host or address; delete the file to get a new one. Every opt-out also stops
it: when reporting is off, no ID is made up and the file is neither read nor written.
It is on by default and configured by the usage-reporting.* properties in
application.properties, each with an environment override:
| Property | Environment variable | Default |
|---|---|---|
usage-reporting.enabled |
USAGE_REPORTING_ENABLED |
true |
usage-reporting.endpoint |
USAGE_REPORTING_ENDPOINT |
https://trace.danielstephenson.dev |
usage-reporting.key |
USAGE_REPORTING_KEY |
the bundled program key |
Set USAGE_REPORTING_ENABLED=false to turn it off; every opt-out is listed under
Usage reporting below.
The write paths that lock a row before deciding on it (moving an entity, removing a placement)
hold a pooled connection for as long as they wait for that lock. Two database.* properties
bound that, and are set as a pair: the longest any statement waits for a row lock, and the size
of the pool the wait is drawn against.
| Property | Environment variable | Default |
|---|---|---|
database.lockTimeoutMs |
DATABASE_LOCK_TIMEOUT_MS |
5000 |
database.maxPoolSize |
DATABASE_MAX_POOL_SIZE |
10 |
A request whose lock wait runs past the bound is answered 503 Service Unavailable with a
Retry-After header rather than held; the data is untouched and the request can be retried.
The lock timeout is applied as lock_timeout on every connection the pool opens, so it must
stay below the pool's 30-second connection timeout. 0 leaves the database's own default in
place, which for PostgreSQL means waiting indefinitely. No statement_timeout is set: the
environment cascade delete issues one statement per entity, location and grid, and a blanket
limit tuned for single-row requests would break the deletion of a large environment.
docker-compose up --build
API will be available at: http://localhost:9999
Once running, you can view the interactive API docs:
http://localhost:9999/swagger-ui.html
or refer to the docs/openapi/viron-api.json file.
The Swagger UI of a locally built Viron (mvn package, run with --server.port=19999 against a throwaway PostgreSQL, usage reporting off), captured with headless Chromium.
Every endpoint except /actuator/health and these API docs requires a JWT issued by the
UserAuth service, sent as Authorization: Bearer <token>; a request without a valid one is
answered 401 Unauthorized. The Postman collection under postman/ sends the token from its
authToken variable.
A token can be limited to reading, so that a viewer (a browser page, or a desktop client on someone else's machine) holds a credential that cannot change or delete anything:
Token's scope (or scp) claim |
GET / HEAD routes |
Every other method |
|---|---|---|
no scope claim (what UserAuth issues today) |
allowed | allowed (unchanged) |
includes viron:read, not viron:write |
allowed | 403 Forbidden, WWW-Authenticate: Bearer error="insufficient_scope" |
includes viron:write |
allowed | allowed |
| other scopes only | allowed | allowed |
Existing tokens keep exactly the access they had; only a token that asks for viron:read alone
is narrowed. The check runs before any controller, so a refused write touches nothing.
scripts/mint_read_token.py mints one with the standard library only, signed with the same
JWT_SECRET Viron validates against. The secret is read from the environment, never from the
command line:
JWT_SECRET=... python3 scripts/mint_read_token.py --subject arcade-viewer --days 30
A read-only token is still a credential: it can read every environment. Give it a lifetime that matches how long the viewer needs it.
Viron sends no CORS headers by default, so a page on another origin cannot read it. To let one read, list its origin:
VIRON_CORS_ALLOWED_ORIGINS=https://arcade.example.com,http://localhost:8000
(property viron.cors.allowed-origins). Each entry is an exact origin, scheme://host[:port]
with no path; * is refused at start-up. When the list is set:
- pages on those origins may send
GETandHEADto/api/v1/**and/actuator/health, with anAuthorizationheader;Retry-Afteris exposed to them; - a preflight (
OPTIONS) is answered before authentication, so it needs no token; - no write method is ever allowed cross-origin: a preflight for
POST,PUT,PATCHorDELETEand an actual cross-origin write are both answered403, whatever the token; - no cookies are involved (
Access-Control-Allow-Credentialsis never sent).
Requests without an Origin header (curl, drivers, the client SDKs) are not affected. A page
can still be handed any token, so give it a read-only token: CORS keeps
the page itself to reads, and the scope keeps the token to reads wherever else it ends up.
Run all Java unit and integration tests:
mvn test
Install the Python client's dependencies (requires Python 3.8+):
pip install -r requirements.txt
Run the Python client tests:
pytest
pytest.ini puts the repository root on the path, so the client is imported the same way
from tests as it is from application code (src.main.python.preponderous.viron...). The
pythonpath setting it uses arrived in pytest 7.0, which is why requirements.txt asks for
that release or newer.
CI runs both suites: the Java build and tests, and the Python client tests on Python 3.8 and 3.12.
Usage reporting is on by default: on start-up the service sends one startup event (program
name viron, version, tag service=true) to the trace service at
https://trace.danielstephenson.dev. Nothing is sent per request, and nothing about users,
hosts, addresses or data is ever included.
Turn it off any of these ways:
USAGE_REPORTING_ENABLED=falsein the environment (usage-reporting.enabled=falseinapplication.properties;compose.ymlpasses the variable through)TRACE_USAGE_REPORTING=offin the environment (alsofalse,0,no; shared by every program that reports to trace, and it wins over the setting above)DO_NOT_TRACK=1in the environment (alsotrue,yes; see consoledonottrack.com)
One line is logged at start-up saying whether reporting is on and, if it is off, which switch turned it off. Details: https://danielstephenson.dev/usage-reporting
This project is licensed under the MIT License.
Copyright Β© 2022-2025 Daniel McCoy Stephenson. All rights reserved.
For inquiries, feature requests, or contributions, please open an issue or reach out via the official GitHub repository:
https://github.com/Preponderous-Software/Viron

