core

A variant of Infrastructure Asset Registry.

View the interactive variant page →

Infrastructure Asset Registry

Part of the worlds-biggest-software-project initiative.

An open-source, AI-native platform for managing the full lifecycle of physical infrastructure assets -- from inventory and condition assessment to capital planning and compliance reporting -- without vendor lock-in to proprietary GIS platforms.

Infrastructure Asset Registry provides municipalities, transportation departments, and utilities with a structured, GIS-linked inventory of roads, bridges, pipes, culverts, signals, and other long-lived assets. It replaces reactive, complaint-driven maintenance with condition-based priority planning backed by data. The system records location, condition, maintenance history, and financial data for every asset, connecting inspection workflows to capital improvement programmes so agencies can make defensible investment decisions.


Why Infrastructure Asset Registry?

  • Proprietary GIS lock-in is the norm. Nearly every incumbent (Trimble Unity, OpenGov/Cartegraph, AssetWorks) requires Esri ArcGIS licences, significantly increasing total cost of ownership. No open-source alternative with comparable condition management depth exists today.
  • Enterprise platforms price out small agencies. IBM Maximo and Infor EAM require specialist consultants and six-figure implementation budgets, putting modern asset management out of reach for smaller municipalities and school districts.
  • Incumbent mobile apps are unreliable. OpenGov/Cartegraph's mobile app is reported as battery-draining and crash-prone; frequent UI changes create retraining burdens for field crews who need simple, stable tools.
  • Compliance reporting is manual and fragmented. GASB 34 and grant-application reporting requires assembling data across multiple systems. No incumbent offers automated, data-driven compliance report generation out of the box.
  • AI capabilities are siloed or absent. Only iFactory and IBM Maximo offer meaningful AI, but iFactory is a prediction overlay (not a full registry) and Maximo's AI is locked inside IBM's ecosystem. Transparent, explainable AI for condition assessment remains an open gap.

Key Features

Asset Inventory and Hierarchy

  • Unique identification and classification for every asset with full attributes: type, location, dimensions, material, installation date, replacement cost, responsible department
  • Hierarchical organisation from network to segment to component
  • Support for both horizontal (roads, pipes, sewers) and vertical (buildings, facilities) asset types
  • Import wizard with AI-assisted field mapping for migrating legacy spreadsheet, GIS, and paper records

GIS Integration (Vendor-Neutral)

  • Spatial data linking via open standards: GeoJSON, OGC API Features, PostGIS
  • Map-based search, proximity analysis, and network-level views without Esri licence dependency
  • Compatible with QGIS, OpenStreetMap, Leaflet, and OpenLayers
  • Near real-time data synchronisation with external GIS sources

Condition Inspection and Rating

  • Mobile-friendly inspection recording with photo capture and offline support for remote field locations
  • Standardised scoring frameworks: Pavement Condition Index (PCI/ASTM D6433), Bridge Health Index (NBI), CCTV pipe grading, Asset Condition Index (ACI)
  • Assisted condition scoring proposed from inspection photos, always confirmed or overridden by the inspector
  • Multi-standard condition index support in a unified scoring model

Capital Planning and Scenario Modelling

  • Replacement cost forecasting by asset class and condition band
  • Scenario modelling showing long-term funding impact of different investment levels, including cost of deferral
  • Candidate projects ranked by a transparent priority score reflecting condition, criticality and risk
  • Predictive deterioration modelling with maintenance deferral cost analysis

Work Order Management and Maintenance

  • Work order creation, assignment, tracking, and completion
  • Preventive maintenance scheduling with automated work order generation
  • Work orders raised directly from the inspection that found the defect, carrying both the asset and that inspection
  • Job costing and cost tracking per asset

Reporting and Compliance

  • GASB 34 and ISO 55001-aligned reporting templates
  • Automated grant-application report generation
  • AI-synthesised plain-language reports for elected officials and non-technical stakeholders
  • Natural language query interface for asset data retrieval and ad-hoc reporting

Community and Stakeholder Access

  • Citizen/constituent issue submission portal linked to asset records
  • Community-facing transparency dashboards showing asset conditions and planned capital works
  • Role-based access control for field crews, supervisors, administrators, and finance staff

AI-Native Advantage

Unlike incumbents that bolt AI onto legacy platforms or offer it only as a proprietary overlay, Infrastructure Asset Registry integrates AI as a core capability -- and keeps it answerable. Assisted condition assessment proposes a score from inspection photographs, with a confidence indicator and the visual evidence behind it; the inspector confirms or overrides it, and the override is recorded. Deterioration models forecast future condition from condition history, asset age and asset class. Natural language interfaces let non-technical staff query asset data and generate reports without BI skills, within their own permissions and traceable to the records. Assisted import mapping proposes how legacy spreadsheet columns map onto registry fields, with manual mapping a first-class path.

All four require model access you supply as configuration. Without it they report themselves unavailable and every other capability is unaffected -- no degraded scoring, no partial results, no failures elsewhere.


Tech Stack & Deployment

  • GIS foundation: PostGIS, GeoJSON, OGC API Features -- no proprietary GIS licence required
  • Frontend mapping: Leaflet or OpenLayers for browser-based map views; QGIS integration for advanced spatial analysis
  • Mobile: Offline-capable web experience for field crews in low-connectivity environments, installable to a phone's home screen -- no app-store distribution
  • Deployment: Self-hosted, cloud, or hybrid
  • Integration: REST API for connectivity to financial systems, ERP (SAP, Oracle, Munis, Tyler), and SCADA
  • Standards: PCI (ASTM D6433), NBI bridge inspection methodology, ISO 55001 asset management framework, GASB 34 financial reporting

Market Context

The global infrastructure asset management software market serves municipalities, transportation departments, and utilities managing assets worth billions in replacement value. Incumbent pricing follows annual SaaS subscriptions tiered by asset count or municipality population, with enterprise platforms like IBM Maximo and Infor EAM requiring substantial implementation services on top of licence fees. Primary buyers are public works directors, asset management coordinators, and municipal CIOs under increasing regulatory pressure from GASB 34 requirements and federal/state infrastructure funding programmes that mandate demonstrated asset management capability.


Running It

docker compose up -d                     # PostGIS 16-3.4 :54439, Redis :63439, MinIO :59439
cd target && uv sync
cp ../.env.dev.example ../.env.dev        # destinations never layer; this file is the whole config
uv run --env-file ../.env.dev alembic upgrade head           # the release: step's own command
uv run python scripts/seed_frameworks.py --destination dev   # PCI, NBI as data
uv run python scripts/seed.py --destination dev              # a populated demo agency
uv run --env-file ../.env.dev honcho start                   # API on :30439, Celery worker, beat
curl -s localhost:30439/healthz          # {"status":"ok","db":true,"redis":true,"storage":true}

New here? user-docs/READ-ME-FIRST.md · Full walkthrough: user-docs/getting-started.md · Demo data, per destination: user-docs/demo-data.md · First use of a real system: user-docs/production-initialisation.md · Tests: user-docs/testing.md · Commands: user-docs/useful-commands.md · API: user-docs/api-reference.md and the generated OpenAPI 3.1 document.


Configuration

Every option the application reads is listed below. There is exactly one place that reads configuration — Settings in target/src/iar/config.py — and no service anywhere in the codebase touches os.environ, which a contract test enforces by scanning the source tree.

There is no DATABASE_URL and no REDIS_URL. The platform injects discrete DATABASE_HOST/_PORT/_NAME/_USER/_PASSWORD/_SSL and REDIS_HOST/_PORT/_NAMESPACE/_TLS variables and the application composes its connection strings from them. A settings object with a database_url field works on a laptop and silently fails to connect on every deployed destination.

Destinations never layer. .env.<destination> is the only file consulted for that destination — there is no base file underneath it. Every key a destination needs appears in its own template, which is why the three templates repeat each other rather than sharing a common part:

DestinationTemplateNotes
dev.env.dev.examplerun from the IDE against the compose stack
prod-parity.env.prod-parity.examplelocal full-parity compose deployment
aws.env.aws.exampleplaceholders only; secrets come from the platform

On aws the whole DATABASE_* and REDIS_* namespace, the S3 bucket and credentials, and AUTH_CLIENT_SECRET are deliberately absent from the template. The platform injects them, and a key in the file that the platform also injects shadows the injected value — which points the application at the wrong database with no error at all. Their defaults, shown below, apply there.

Runtime

OptionDefaultWhat it does
APP_NAMEinfrastructure-asset-registryService name in every log line.
APP_ENVlocalFree-text environment label; appears in logs.
APP_URL(empty)The externally reachable base URL, used to build absolute links and the sign-in callback. Declare it only where the hostname is known before the application runs — every destination except demo and sandbox, whose hostname is minted at launch. Left empty it falls through to NEXT_PUBLIC_APP_URL, then AUTH_URL, then http://localhost:8080.
LOG_LEVELINFORoot log level. Logs are one-line JSON on stdout, mapped to the OTel Logs Data Model.
PORT30439 locally, 8080 in a container8080 is the platform contract and is what every container listens on — a framework default of 3000 or 8000 works locally and is unreachable when deployed. A process you start yourself is a different case: it shares the machine with every other project, so it uses this project's derived port, 30439, which is also what the compose stack publishes. Point the web dev server at a port another project's API holds and every request in the application reaches that application, which answers a well-formed "not found" to paths it has never heard of.
IAR_WEB_DIST(unset)Directory holding the built SPA. Unset means web/dist relative to the working directory, which is what the container image provides.
BASEMAP_STYLE_URL(unset)A MapLibre style JSON drawn beneath the agency's assets, so the map reads as a place — streets, water, names — rather than as lines on a plain surface. Offered, never required: unset means the plain surface and no fetch to anywhere but this agency's own tiles, which is what Principle I demands of the product. The dev lane names an open style (OpenFreeMap: OpenStreetMap data, no key, no limits); an agency may name any style it already uses. The demo and sandbox lanes name a bundled district extract BUILT INTO THE IMAGE by scripts/fetch_basemap.py and served from /basemap/** — a same-origin path, not a host, because those lanes launch on a hostname minted at launch and a CDN-hosted style would be third-party egress a public demo instance must not have (ready-for-sandbox vgcgj7). Must be an http(s) address or a same-origin path starting with /; anything else is refused at startup. See docs/basemap-decision.md.
WBSP_SAMPLE_MODE(empty)Injected by the platform, never set by hand. true on every demo/sandbox pod launched from wbsp.ai; absent everywhere else. Read through Settings.sample_mode, it attaches a newly signed-in visitor to the seeded demonstration agency (auth/provisioning.py) and refuses every outbound call this project can make on a visitor's behalf — email, external geographic sync, and assisted (AI) capabilities — regardless of whether a relay, model or key happens to also be configured. See the ready-for-sandbox checklist, item 8r79ad.
NEXT_PUBLIC_APP_URL(empty)Injected by the platform, never set by hand. The URL of a launched demo or sandbox instance. That hostname is minted when the instance starts, so nothing can declare it in advance — which is why APP_URL is left unset on those lanes and why declaring it there once returned every visitor's completed sign-in to a different deployment's hostname, where nothing answers. Read through Settings.public_base_url, which prefers APP_URL, then this, then AUTH_URL. See the ready-for-sandbox checklist, item azfe9u.
AUTH_URL(empty)Injected by the platform, never set by hand. The same launched-instance URL under the second name the launcher supplies it by. Read through Settings.public_base_url.

Database

DATABASE_SSL=true is honoured with a real TLS context — RDS injects it as true. Credentials are percent-encoded when the DSN is composed, because an unescaped @ in a platform-generated password would otherwise point the application at a different host entirely.

OptionDefaultWhat it does
DATABASE_HOSTlocalhostInjected by the platform.
DATABASE_PORT5432Injected by the platform.
DATABASE_NAMEiarInjected by the platform.
DATABASE_USERiarInjected by the platform.
DATABASE_PASSWORD(empty)Injected by the platform. Never committed.
DATABASE_SSLfalsetrue opens the connection with a verified TLS context.
DATABASE_POOL_SIZE10Connections kept open per process.
DATABASE_MAX_OVERFLOW10Additional connections allowed under burst.
DATABASE_ECHOfalseLogs every SQL statement. Keep it false outside a debugging session: it is the fastest way to get row values into the log.

Redis

Redis is a broker and a cache only, declared durable: false. Every job's source of truth is the Postgres jobs table and every task is idempotent, so a queue entry lost to a failover is re-derivable.

OptionDefaultWhat it does
REDIS_HOSTlocalhostInjected by the platform.
REDIS_PORT6379Injected by the platform.
REDIS_DB0Logical database index. Managed Redis often exposes only 0; REDIS_NAMESPACE is what actually keeps applications apart.
REDIS_NAMESPACEiarMandatory key prefix applied to every key the application writes — cache entries, rate-limit buckets, and the Celery broker's global_keyprefix. Cache-mode Redis may be shared. Written as namespace:{namespace}:…, which satisfies two constraints at once: the platform grants this deployment's cache user a prefix match on the bare namespace, so a key must literally begin with it, and a clustered cache hashes only what is inside the first braces, so the repeat keeps one deployment's keys on one slot. Wrapping the namespace instead satisfies only the second, and the cache then refuses every key with a message that mentions neither keys nor patterns.
REDIS_TLSfalsetrue selects the rediss:// scheme, and adds the certificate handling that an encrypted address must state. It states none: encrypted in transit, issuer unverified, the same trade the database connection makes against the same provider and for the same reason. Stating nothing is not an option — the queue library refuses an encrypted address that is silent about it, and refuses it at startup.
REDIS_USERNAME(empty)The ACL user a cloud Redis provisions. Empty means send no username, which is right for a local Redis whose single default user authenticates by password alone. A named ACL user rejects a password-only sign-in with WRONGPASS — which reads as a wrong password and sends you to check the secret instead of the name.
REDIS_PASSWORD(empty)Injected on a destination whose Redis requires authentication. Empty sends no credentials at all.
REDIS_CLUSTERfalseWhether the instance is clustered, which the application has to be told: no injected variable reports it, and a tenant's Redis mode is resolved across the whole tenant, so another application declaring durable storage clusters the instance this one shares. true builds a cluster client for the cache. It also makes the Celery application refuse to construct, deliberately — kombu's Redis transport does not follow cluster redirections, so a queue on a clustered instance accepts work and runs none of it.

Object storage

Inspection media and rendered reports. Only object keys are stored in Postgres. Locally this is the MinIO service in docker-compose.yaml; on aws it is the bucket declared by s3.enabled, with versioning requested and re-checked at runtime by MediaStore.ensure_versioning().

OptionDefaultWhat it does
S3_BUCKETiar-mediaBucket name. Injected on aws.
S3_REGIONus-east-1Bucket region.
S3_ENDPOINT_URL(unset)Point at MinIO locally; leave empty on aws so boto3 uses the real endpoint.
S3_PUBLIC_ENDPOINT_URL(unset)The endpoint a browser must use to follow a presigned download URL, when it differs from the one the application talks to. Set on the local compose stack (http://localhost:59439), where the app reaches MinIO as minio:9000 and no browser can resolve that name; empty everywhere else. The host is part of the SigV4 signature, so the URL is signed for this endpoint rather than rewritten afterwards.
S3_ACCESS_KEY_ID(unset)Injected on aws (IRSA).
S3_SECRET_ACCESS_KEY(unset)Injected on aws (IRSA).
S3_FORCE_PATH_STYLEfalsetrue for MinIO, which does not do virtual-host-style addressing.
MEDIA_MAX_BYTES26214400 (25 MB)Largest accepted inspection photo.

Authentication

Sign-in goes through the platform HAP (OAuth 2.0 + OIDC, PKCE S256). The application never presents an isolated app-only login to a user who already holds a platform session, and users are keyed on sub, never on email.

AUTH_ISSUER on aws is an unresolved placeholder. The platform documentation gives wbsp-auth.wbsp-demo.com and the HAP documentation gives auth.wbsp-demo.com for what is described as the same service (TODO(HAP_ISSUER_HOSTNAME), constitution §Unresolved Platform Constraints). Confirm with the operator before the first aws deploy. Do not guess: a plausible wrong issuer deploys cleanly and fails every sign-in.

OptionDefaultWhat it does
AUTH_ISSUER(empty)OIDC issuer URL. Empty disables authentication wiring entirely.
AUTH_CLIENT_ID(empty)OIDC client id.
AUTH_CLIENT_SECRET(empty)OIDC client secret. Declared under secrets: in wbsp.yaml; a declared secret with no value fails the deploy, which is the intended fail-closed behaviour.
AUTH_REDIRECT_URI(empty)Must match the registered redirect URI exactly.
AUTH_POST_LOGOUT_REDIRECT_URI(empty)Where the HAP returns the browser after sign-out.
AUTH_SCOPESopenid profile emailScope list, comma- or space-separated. The committed templates use commas: uv run --env-file abandons parsing an entire file at the first unquoted space and loads nothing but a warning, at which point every key silently takes its default — including SESSION_COOKIE_SECURE.
SESSION_SECRETchange-meSigning key for the session cookie. Replace on every deployed destination.
SESSION_COOKIE_NAMEiar_sessionConfigurable so two deployments on sibling hostnames cannot overwrite each other's cookie.
SESSION_COOKIE_SECUREtrueLocal destinations set this false deliberately: browsers drop a Secure cookie over http://localhost, and the symptom is a sign-in loop with no error.
SESSION_MAX_AGE_SECONDS28800 (8 h)Session lifetime — a working day, after which the HAP is asked again.
AUTH_DEV_LOGINfalseLocal unattended runs only. Signs in as one named existing account without an OIDC round trip, so the Impeccable pass, the E2E suite and the screenshot capture can reach authenticated views with nobody at the keyboard. Enabling it on a non-local APP_ENV is a startup failure, not a warning. See user-docs/login-bypass.md.
AUTH_DEV_LOGIN_SUB(empty)Which existing account AUTH_DEV_LOGIN signs in as, by sub. No default: a bypass that picks an account for you is one nobody can audit. It grants no authority — roles are read from the database per request, exactly as for a real session.
AUTH_HAP_ISSUER(empty)Injected by the platform, never set by hand. The shared demo HAP issuer, supplied to every demo/sandbox pod at launch. Do not put it in a .env file and do not declare it under secrets: — a demo deploy is fail-closed on declared secrets, so declaring credentials the platform supplies blocks the lane on values nobody can look up.
AUTH_HAP_ID(empty)Injected alongside AUTH_HAP_ISSUER. The shared demo client id, whose redirect is a wildcard across *.wbsp-demo.com — which is what makes sign-in work on a sandbox URL without registering one per host.
AUTH_HAP_SECRET(empty)Injected alongside AUTH_HAP_ISSUER. When all three are present they replace AUTH_ISSUER / AUTH_CLIENT_ID / AUTH_CLIENT_SECRET at startup; when any is missing the baked-in client is kept, so a half-injected client cannot break a working configuration.

Email — optional

OptionDefaultWhat it does
SMTP_HOST(unset)Unset means notifications are in-app only and the API reports email_status: skipped_no_config (FR-079). Nothing else degrades.
SMTP_PORT(unset)
SMTP_USER(unset)
SMTP_PASSWORD(unset)
SMTP_FROM(unset)Required alongside SMTP_HOST for email to be considered configured.
SMTP_TLStrueSTARTTLS.

Assisted capabilities — optional

With no model access configured, GET /api/v1/capabilities reports every capability false, each AI endpoint returns 503 capability_unavailable, and everything else in the product works normally (FR-051). That is a tested contract, not a degraded mode.

OptionDefaultWhat it does
AI_MODEL_CHAT(unset)Enables natural-language query and report synthesis.
AI_MODEL_VISION(unset)Enables computer-vision condition assessment.
AI_API_KEY(unset)Provider credential.
AI_API_BASE(unset)Override for a self-hosted or proxied endpoint. Access routes through a provider-neutral abstraction; no single AI vendor is ever required to run the system.
AI_VISION_CONFIDENCE_THRESHOLD0.75Below this an assessment is flagged requires_human_review. Vision scores are advisory and always overridable, with the override recorded.

Limits and pagination

OptionDefaultWhat it does
RATE_LIMIT_AUTHENTICATED_PER_MINUTE300Per user (FR-066). Exceeded → 429 with Retry-After.
RATE_LIMIT_PUBLIC_PER_MINUTE20Per IP on the citizen surface (FR-039).
PAGINATION_DEFAULT_LIMIT50Page size when the caller names none.
PAGINATION_MAX_LIMIT500Hard cap on limit.

Continuity

OptionDefaultWhat it does
BACKUP_BASISmanagedWhich facility recovery rests on. managed uses the platform's own backups as the RPO basis, with this application owning automated restore verification. self_managed_dump activates the application-owned pg_dump-to-S3 contingency, so a withdrawn PostGIS grant is a configuration change rather than a rewrite. See user-docs/backup-restore.md.

Project Status

This project is in the research and specification phase.
Contributions, feedback, and domain expertise are welcome.


Contributing

We welcome contributions from developers, domain experts, and potential users. See CONTRIBUTING.md for guidelines.

Important: All contributions must be your own original work or clearly attributed open-source material with a compatible licence. Copyright infringement and licence violations will not be tolerated and will result in immediate removal of the offending contribution. If you are unsure whether a piece of code, text, or other material is safe to contribute, open an issue and ask before submitting.


Licence

Licence to be determined. See discussion for context.