Status: the scripts side (§4.1, §6.1) is implemented as of 2026-09-25: OBP_ENTITY_SPACE_ID
and obp_space.py, with create_space_bank.py creating the bank if it is missing.
OBP_ENTITY_PREFIX was removed from this repo, which settles risk 2 in favour of spaces.
§7 is out of date: record Roles are now Can<Action>DynamicEntityRecord_<entity> at every
level, granted at the bank id (SYS for system level), so the name no longer changes.
The OGCR-App side (§4.2, §6.2) is not done.
Written: 2026-09-21. Verified against: http://localhost:8080 (OBP, dynamic entity API v5.1.0).
The idea: use an OBP bank — aka Space — as a namespace for a version of the OGCR
schema, so /banks/ogcr_v1/... and /banks/ogcr_v2/... can hold the same entity names
with different definitions and separate rows, side by side.
Space rule (decided): an OGCR deployment is either entirely system-level or entirely bank-level. No per-entity mixing — one switch, one URL shape.
Probed with throwaway entities on the local instance (all deleted afterwards). These are facts about this OBP build, not assumptions:
| Question | Result |
|---|---|
| Same entity name, different schema, at two banks | ✅ both 201 |
| Rows isolated per bank | ✅ a v2-only field existed only at the second bank |
| Same name also at system level, concurrently | ✅ all three coexist |
reference: between two bank-level entities in one bank |
✅ kept, not downgraded |
reference: from a bank entity to a system entity |
✅ kept, and enforced (invalid FK → 400) |
| Public read at bank level | ✅ works — see the path warning below |
Cross-level references are recorded for completeness; the all-or-nothing space rule means we don't rely on them.
All four dynamic-entity paths follow one pattern:
/obp/dynamic-entity/[banks/BANK_ID/][public/]ENTITY
| Authenticated | Public | |
|---|---|---|
| System | /obp/dynamic-entity/ENTITY |
/obp/dynamic-entity/public/ENTITY |
| Space | /obp/dynamic-entity/banks/BANK_ID/ENTITY |
/obp/dynamic-entity/banks/BANK_ID/public/ENTITY |
The space prefix comes first; public always sits immediately before the entity name.
Worth knowing when implementing the path builder: a malformed variant such as
/obp/dynamic-entity/public/banks/BANK_ID/ENTITY returns 404, which is
indistinguishable from "entity missing" or "public access off" — so a URL bug there
will look like a data or permissions problem.
Every OGCR entity — domain entities and the reference vocabularies country and
technologies_practices_processes — lives at the configured space:
| Configuration | Entity path | Public read path |
|---|---|---|
| System (today) | /obp/dynamic-entity/ENTITY |
/obp/dynamic-entity/public/ENTITY |
| Bank | /obp/dynamic-entity/banks/BANK_ID/ENTITY |
/obp/dynamic-entity/banks/BANK_ID/public/ENTITY |
Consumers migrate between versions by changing one path segment; old and new stay live in parallel.
Consequence of the space rule: each version carries its own copy of the
vocabularies (249 countries, 28 practices) at its own public URL. That is cheap to
create — create_example_data.py --fixtures-only populates them — but it means the
open-data URL is version-specific by design, not stable across versions.
OGCR-App already uses bank_id to mean something else.
src/lib/obp/currentBank.ts stores a user-selected bank as the OBP user attribute
CURRENT_BANK_ID, deliberately shared with OBP Portal and API Manager, and used by
/trading. If a "version bank" were read from the same place, a user changing banks
in the picker would silently change which schema version they see.
Therefore: the entity space is a configuration-level value that is never the
user's current bank. That drives the env var naming below — do not call it
OBP_BANK_ID.
OBP already uses Space as an alias for Bank, so the terminology is borrowed rather
than coined. Evidence in OBP-API-Simon/OBP-API:
| Source | Shows |
|---|---|
code/model/dataAccess/AuthUser.scala:976 |
"If a User creates a Bank (aka Space) the user can create and modify Dynamic Endpoints and other objects in that Bank / Space." |
code/api/v4_0_0/JSONFactory4.0.0.scala:709 |
case class MySpaces(bank_ids: List[String]) — a Space is a bank id |
code/api/v4_0_0/Http4s400.scala:3818 |
GET /my/spaces — the alias is already on the API surface |
props/sample.props.template:1608 |
"the bank_ids (Spaces) the User has access to", prop email_domain_to_space_mappings |
So OBP_ENTITY_SPACE_ID reads naturally to anyone who knows OBP, avoids the
CURRENT_BANK_ID collision in §3, and matches the direction of travel if the URL
segment gains a spaces alias (§5).
One variable, the same name in both repos, and the values must agree.
| Variable | Default | Meaning |
|---|---|---|
OBP_ENTITY_SPACE_ID |
(empty) | The bank id owning this deployment's entities, e.g. ogcr_v2. Empty = system level = today's behaviour. Single switch; unsetting it is the rollback. |
The URL segment (banks, later spaces — §5) is a constant in the path builder, not
configuration: it is the same on every instance, so making it an env var would only
create a way for two deployments to disagree.
OBP_ENTITY_PREFIX stays as it is — see risk 1.
The value is an OBP bank id;
SPACEis OBP's own alias for it (§3.1), chosen here because it is unambiguous against the user-selectedCURRENT_BANK_ID.
| Variable | Scope | Default | Meaning |
|---|---|---|---|
OBP_ENTITY_SPACE_ID |
private ($env/dynamic/private) |
(empty) | Must match the scripts' value for the version being served. Not the user's CURRENT_BANK_ID. |
PUBLIC_OBP_ENTITY_SPACE_ID |
public | (omit initially) | Only if the browser ever builds a dynamic-entity URL itself. All current access is server-side, so leave it out until a page needs it. |
Follow the repo's existing convention: private vars via $env/dynamic/private
(as src/lib/constants/entities.ts already does for OBP_ENTITY_PREFIX), PUBLIC_
only for what the client must see. Add it to .env.example with a comment — that
file is the de-facto deployment documentation.
OBP currently hardcodes banks in both the data and management paths:
data: /obp/dynamic-entity/banks/BANK_ID/ENTITY
management: /obp/v5.1.0/management/banks/BANK_ID/dynamic-entities
To make the OGCR API surface less banky, the ask is a fixed alias accepted on every
instance — spaces (optionally s as a shorthand) routing to the same handlers —
not a per-deployment configurable segment.
A configurable segment would make every URL deployment-specific: shared client code,
examples, docs and cross-instance scripts would all need parameterising, and two OBP
instances would stop speaking the same URL language. A fixed alias gets the same
cosmetic result with none of that, and it finishes an alias OBP has already started
(GET /my/spaces, the email_domain_to_space_mappings prop, "Bank (aka Space)" in
AuthUser.scala — see §3.1).
Consequently the segment is a constant in each repo's path builder, not an
environment variable. Switching from banks to spaces once OBP accepts it is a
one-line code change in two places, applied together.
If the alias is added, it should cover both the data and management paths, so the two builders stay consistent.
| File | Change |
|---|---|
obp_dynamic_api.py |
One path builder used everywhere: management URL becomes /management/banks/<id>/dynamic-entities when a space is set; same for update/delete by id. Note this is a genuine branch, not a prefix — the system form uses a different noun (system-dynamic-entities). |
create_example_data.py |
Object paths become /obp/dynamic-entity/banks/<id>/<entity> — a pure prefix; existing_object_ids too. |
parse_minimum_fields.py |
Thread the space through both create passes and --update. |
delete_dynamic_entities.py, get_and_delete_dynamic_entities.py |
List and delete within the space only — must never touch another version's entities. |
create_entitlements.py |
Grant bank-level roles (§7) — currently system-only. |
recreate_dynamic_entities.sh |
Pass the space through; refuse to run if the bank doesn't exist, so a version is never half-created. |
Because the space is all-or-nothing, every one of these is the same mechanical edit: replace a hardcoded path with the shared builder. No per-entity branching anywhere.
| File | Change |
|---|---|
src/lib/constants/entities.ts |
Add dynamicEntityPath(entity) returning the system or space path. Single chokepoint, alongside the existing ENTITY_PREFIX logic. |
~30 call sites of /obp/dynamic-entity/... across src/routes/** and src/lib/** |
Replace hand-built paths with the helper. Mechanical but wide. |
src/lib/server/roleChecker.ts, src/lib/utils/roleCheck.ts |
Role names differ at bank level (§7); derive from the configured space rather than hardcoding _System. |
src/lib/obp/currentBank.ts |
No change — must stay independent. Worth a comment saying so, to stop a future reader wiring them together. |
.env, .env.example |
Add the vars from §4.2. |
Bank-level roles drop System and are held at the bank:
| Level | Role name |
|---|---|
| System | CanGetDynamicEntity_System<entity> |
| Bank | CanGetDynamicEntity_<entity>, granted at BANK_ID |
Consequence: every version bank needs its own grants for every user and entity.
With N entities and M versions that is N × M grants per user. create_entitlements.py
must loop over spaces, and onboarding a user to a new version becomes an explicit
step. This is the largest recurring operational cost of the approach.
There is a precedent worth asking OBP to extend. The email_domain_to_space_mappings
prop already auto-grants roles per space: a user whose validated email domain maps to a
set of bank_ids is granted the roles for those spaces on login and on DirectLogin
token generation (AuthUser.scala:1010-1013). Today it covers Dynamic Endpoint
roles only — listOfRolesToUseAllDynamicEndpointsAOneBank reads from
DynamicEndpointProvider (DynamicEndpointHelper.scala:169), not from dynamic
entities. Extending the same mechanism to Dynamic Entity roles would remove most of
the sprawl above, and is a small, well-precedented OBP change to request.
Note the path segment (§5) does not change role names — they are not path-derived.
-
A dropped prefix reads different data, silently. The space prefix does not scope a shared resource — it selects a different entity that happens to share a name, with its own schema and rows (§1 confirms three same-named entities can be live at once). So:
Mistake Result Malformed path 404— loudPrefix dropped entirely 200with another version's data — silentEvery OGCR entity is system-level today. If a space is adopted and the system-level entities are left in place, any un-migrated call site keeps working while quietly reading v1. Mitigations: (a) delete the system-level entities after cutover, so a dropped prefix 404s; (b) assert on the response envelope — space responses carry
"bank_id": "<id>", system responses carry no such key, so the app's path helper can verify which namespace actually answered. -
Two namespacing mechanisms.
OBP_ENTITY_PREFIXalready exists and is wired through both repos. Prefix and space would be two ways to say the same thing. Decide which owns versioning; document the other as not-for-that-purpose. -
Entitlement sprawl (§7).
-
No automatic data migration. A new version starts empty. Vocabularies are trivial (re-run the fixtures), but operator/activity/parcel rows need an explicit, written copy step — including how in-flight records are handled.
-
No cross-version references. A v2
activitycannot reference a v1parcel. Usually desirable; confirm it doesn't break a real workflow. -
Semantic overloading of "bank" (§3), plus unknown interactions with consents, scopes and bank-scoped OBP tooling. Untested. The §5 rename reduces the cosmetic part of this, not the underlying one.
-
Public open-data URLs become version-specific (§2). Accept, or publish a stable alias outside OBP.
-
Chain/tokenizer side.
OGCR-chain-cacheand the tokenizer also read OBP entities and inherit the same path and role changes. Not scoped here.
- Decide §3 naming and risk 1.
- Scripts first, behind the env var. With
OBP_ENTITY_SPACE_IDempty, behaviour is byte-for-byte what it is today — verify that before going further. - Create the version bank and grant entitlements (§7).
- Populate the space: entities from the spreadsheet, then
create_example_data.py --fixtures-onlyfor the vocabularies. - App: path helper + role names + env, still defaulting to system level.
- Cut over one environment by setting
OBP_ENTITY_SPACE_IDin both repos. Rollback = unset it.
| Option | Verdict |
|---|---|
Additive schema evolution — only add optional fields, update in place via --update |
Far less work; no new concepts. Preferred unless concurrent schemas are genuinely required. Does not help when a field must change type or be removed. |
OBP_ENTITY_PREFIX per version (v2_operator) |
The mechanism already exists in both repos — the cheapest real versioning available today, and no OBP change needed. Pollutes the entity catalogue and role list. Worth comparing seriously before committing to spaces. |
Entity name suffix (country_v2) |
Same as above without the prefix plumbing; every consumer hardcodes the version in the entity name. |
| Version field on rows | Solves data versioning, not schema versioning. Not applicable. |
The deciding question is narrow: do old and new schemas need to be live at the same time, with consumers migrating at their own pace?
- Yes → space-level entities, as specified above. The evidence in §1 says it works.
- No → additive evolution through the existing
--updatepath; revisit later.