Your data stays portable — you are never trapped in our format.
Runs where you need it
🐳
Containerized
The whole stack is containers — one command to bring it up.
🔌
Air-gappable
No required calls home; runs fully on a private network.
🖥️
Your hardware
On the servers you already own, in your own data centre.
On-prem by design — sovereignty isn't an add-on, it's the default.
04
How a run works
From a click to a catalogued result
1 · Trigger
Button or cron
A run is created — by a person or on schedule.
2 · Admit
Scheduler
Quota & single-active checks; the run goes pending.
3 · Spawn
Sandbox
An isolated container with locked-down, scoped access.
4 · Execute
Your Python
Reads inputs, computes the result, writes output.
5 · Publish
Register
Output stored and registered as a catalog item.
6 · Done
In the catalog
Result, logs and stats — visible and reusable.
Every run is a sandbox
🔒
Locked-down
No privileges, non-root, capped CPU/RAM/processes, hard timeout.
🔑
Scoped access
Per-run credentials: read its inputs, write only its own output.
🌐
Network-isolated
Can't reach the database or auth; optional egress allow-list.
Hostile or buggy code can only affect its own run — nothing else.
Reliable by default
🗓️
Scheduling
Cron per job; the scheduler admits runs and respects quotas.
🔗
Job chaining
A finished run fires the next — ingest → prepare → analyze, no external orchestrator.
🚦
No pile-ups
Single-active guard — a slow run never stacks on itself.
📜
Full audit
Structured logs, per-run stats, state for every execution.
05
How analysts build scripts
The contract is tiny
🐍
Your Python
src/ with your code — any libraries you declare.
📄
pyproject.toml
Standard dependencies. The platform builds the environment.
📝
process.yaml
Declares parameters, types and defaults — the UI form is built from it.
▶️
entrypoint
One file to run. It reads params from the environment.
Zip it, upload it — that's a process. No Dockerfile, no infra.
Python, R or Java — same contract
🐍
Python
pyproject.toml + an entrypoint. The platform builds a venv with uv and runs it.
📊
R
renv.lock + an .R entrypoint. renv::restore() rebuilds the exact library set, then runs.
☕
Java
A Maven pom.xml. The platform runs mvn package and launches the built jar.
One run contract for every language: same parameters, same storage, same catalog. Pick the runtime in process.yaml — the platform mounts the right image and dependency cache.
Same result, three languages
🧮
One analytic, ported
The same NDVI demo ships in Python, R and Java — identical inputs, identical contract.
📦
Identical output bundle
Each writes the same product: a COG, a PNG preview, a CSV of stats and a one-page PDF report.
🗂️
One catalog item
All three publish back the same way — the catalog can't tell which language produced it.
Proof the runtime is a choice, not a constraint — your team writes in the language it already knows.
Types drive the UI form — bbox picker, date, number.
Defaults mean a job works with zero config.
category sets the default schedule & behaviour.
No params plumbing — the platform passes them in.
Categories drive behaviour
📥
ingest
Pulls external data into the catalog. Nightly by default.
🧮
preparation
Derives products from catalog data. Runs after ingest.
📊
compute
Analytics that emit results — on demand or scheduled.
Pick a category; sensible scheduling and publishing come for free.
The SDK is just environment variables
import os, json
params = json.loads(os.environ["UPROJ_PARAMS"])
bbox = params["bbox"]
# read inputs straight from object storage via GDAL
src = "/vsis3/inputs/s2-l2a/.../red.tif"
# where to write + how to call back
out_dir = os.environ["UPROJ_OUTPUT_DIR"]
api = os.environ["UPROJ_API_URL"]
token = os.environ["UPROJ_SERVICE_TOKEN"]
Params arrive as JSON — no parsing framework.
Read rasters in place from storage — no download.
A short-lived token authorizes catalog calls.
Plain Python — use whatever libraries you like.
Walkthrough — an ingest script
# 1. search the source for new scenes in the window
scenes = search(bbox, since=window_start)
# 2. mirror each asset into the catalog's storage
for s in scenes:
for asset in s.assets:
copy_to_storage(asset.url, key=object_key(s, asset))
# 3. register the scene as a catalog item
register_item(stac_item(s))
Search → mirror → register. That's ingest.
Already-mirrored scenes are skipped — re-runs are cheap.
Set a start date once to backfill; then it rolls forward nightly.
Walkthrough — a preprocessing script
# read bands in place, compute the index
red = read("/vsis3/inputs/s2-l2a/.../red.tif")
nir = read("/vsis3/inputs/s2-l2a/.../nir.tif")
ndvi = (nir - red) / (nir + red)
# write a cloud-optimized GeoTIFF + register it
write_cog(ndvi, f"{out_dir}/ndvi.tif")
register_item(stac_item("s2-ndvi", scene_id))
Reads parents from storage, writes a COG.
Publishes back into the catalog as a new product.
Same shape for any index, mask or composite you dream up.
Upload — it auto-versions
Each upload is a new immutable version; nothing in production breaks.
Wire a job, set a schedule
Pick the process, fill the form (defaults prefilled), add a cron — done.
06
The platform, hands on
Catalog — browse every input
Inputs and results in one searchable, paginated browser.
Map & analysis
Stack scenes and results on one interactive map — overlay, restyle, inspect.
Maps · Analysis — explore results in place
🗺️
Layer stack
Overlay any number of catalog rasters; drag to reorder, set opacity and colormap per layer.
🎚️
Legends in real units
Each layer's scale reads in physical units; class maps get a labelled chip per category.
🎯
Click to inspect
One click reports the pixel value across every visible layer at once.
🛰️
Basemaps & search
OSM, satellite or topo underlays, with place search to jump anywhere.
Jobs — saved configs + schedule
Each job: a process, its params, an optional cron, a run button.
Runs — every execution tracked
State, logs and the catalogued result for every run.
Secrets — encrypted, per-owner
Credentials stored encrypted, bound to a process input, never echoed back.
Operations dashboard
Containers, disk and the last runs — at a glance for the operator.
Settings — live, no restart
Tune limits, access and categories from the UI; changes apply immediately.
Users & access
Identity backed by OIDC — admins and users, real role-based access.
07
Operations & trust
Security in layers
🧱
Sandboxed runs
Locked-down containers — no privileges, capped, time-limited.
🔑
Scoped storage
Each run reaches only its own data, nothing else.
🌐
Network isolation
Runs can't touch the database or auth; egress is optional & allow-listed.
👤
Identity & RBAC
OIDC sign-in, API tokens, admin vs user — real access control.
Share deliberately, by default private
👤
Owner-scoped
A process is visible to its owner and to admins — nothing is exposed by accident.
🤝
Opt-in sharing
Flip one flag to share a process with the rest of your team.
🌍
Public links, fail-closed
Expose a result or its map tiles by anonymous link only via an explicit allow-list — off until you turn it on.
Every wider audience is a deliberate choice, never the default.
Operate with confidence
📊
Observability
Structured logs, per-run stats, an operator dashboard.
💾
Backups
One-command database dump and a storage inventory.
⬆️
Install & update
Install and update are each a single command — no manual steps.
08
Why uproj
Not a notebook. Not a cloud bill.
📓
vs. notebooks
Repeatable, scheduled, catalogued, multi-user — not a one-off script.
☁️
vs. cloud SaaS
On-prem and sovereign — no per-scene egress fees, your hardware.
🧩
Extensible
Any new source or analytic is just code — Python, R or Java, one simple contract.
Where it goes next
🛡️
Tighter egress
Per-process declared egress allow-lists.
📈
Observability stack
Optional metrics + dashboards for larger deployments.
⚡
GPU compute
Heavier analytics on accelerated runtimes.
Let's build it
Your data, your servers, your analysts — in production next week.