Database Schema
The SDK ships 19 migrations creating 22 tables, all prefixed sdk_. They load from the package — php artisan migrate applies them and there is nothing to publish.
One table the SDK does not create is users. The SDK ships no auth table and makes no assumption about yours; the link is a partner_id column you add to your own table. See Installation.
The invariants that shape everything
Four rules run through the whole schema. Knowing them means you can predict a table's shape before you read it.
1 · Every grant is time-bound, and no timestamp is nullable. Every grant table carries valid_from (defaults to now) and valid_until (defaults to 9999-12-31 23:59:59). There is no "forever" and no "unset" — a grant that has ended carries the moment it ended, which is what makes ui5:explain --at= able to answer questions about last Tuesday. See time-aware grants.
2 · Every grant records who granted it. assigned_by_partner_id on every assignment table, with a restricting foreign key: a partner who has granted something cannot be deleted out from under the audit trail.
3 · Partner identity is enforced inside the SDK. Every partner_id in an sdk_* table is a strict foreign key to sdk_partners.id, cascading on delete. The one nullable, soft link in the system is the users.partner_id bridge in your own table.
4 · Catalog tables are owned by code. sdk_partner_roles, sdk_partner_relationship_types and sdk_address_providers are Customizing catalogs: ui5:sync reconciles them to exactly what your attributes declare, and deletes rows you did not declare. They carry a surrogate id plus a unique business code, and no timestamps — the worker writes through the query builder, not Eloquent.
The registry catalog
What ui5:sync writes from your declarations.
sdk_artifacts — one row per registered artifact. namespace (unique), type (the ArtifactType enum), version, expose, title, description. No foreign key to a module: modules are runtime-only, artifacts are addressed by namespace string.
sdk_abilities — artifact_id, ability, type (the AbilityType enum). Unique per (artifact_id, ability, type). Every ability belongs to exactly one artifact.
sdk_roles — role (unique) and a nullable scope. sdk_ability_role links roles to abilities.
sdk_groups — code (unique) and a description. sdk_group_role and sdk_ability_group link a group to roles and to abilities directly, which is what makes a group a single grantable bundle of both.
sdk_slots — the slot catalog: name (unique), type, default_value (JSON), note, editable, a declared_by_snapshot, and synced_at / removed_at. The removed_at column is why a retired slot leaves a tombstone rather than vanishing.
Security assignments
Three tables, one shape. Each grants one thing to one partner for one window.
| Table | Grants | Unique on |
|---|---|---|
sdk_ability_assignments | one ability | (partner_id, ability_id, valid_until) |
sdk_role_assignments | one role, optionally in a context | (partner_id, role_id, context_type, context_id, valid_until) |
sdk_group_assignments | one group | (partner_id, group_id, valid_until) |
valid_until is part of every unique key, which is what lets the same grant be issued again for a later window without colliding with its own history.
sdk_role_assignments carries the extra pair context_type / context_id — that is role scope: this role, but only for that project. It has its own composite index for the scope lookup, because that query runs on every scoped read.
sdk_delegations is the impersonation table: delegate_partner_id, target_partner_id, granted_by, a note, and the same validity pair. See impersonation.
Settings
sdk_settings carries a scoped value and quite a lot of metadata, because a setting has to be editable by a console that has never seen your code:
artifact_id— whose setting it is;scope(theScopeenum: Platform, Installation, Tenant, Site, User) andsetting— the key;partner_id— set only on aUser-scope row;set_by— the writer, the system actor on a seeded row;value(JSON) andvalue_type— validated separately from storage;level— the edit level shown as a badge;value_help,value_help_scope,model_class— how the console offers a value for aModel-typed setting.
sdk_slot_assignments is the actor layer: (partner_id, slot_name) unique, a JSON value, and assigned_by_partner_id. One value per actor and slot — a write replaces, a clear deletes, and there is no history. See actor slot values.
Partners
sdk_partners is the business actor — person, organisation or department in one table. It carries the identity fields (ext_ref, type, system_level, search_term, name, first_name, avatar_path, gender), the contact fields (email, phone, mobile), the legal-identity block (vat_number, company_registration_number, registered_office), the dates (foundation_date, liquidation_date, birthdate), language_code, and archived.
search_term is an internal matchcode — upper-cased and stripped of punctuation for searching. It is not a display field, and an export that includes it reads as noise.
sdk_partner_roles (catalog) and sdk_partner_role_assignments are the business role axis, separate from the security roles above: who is a customer, a supplier, a key account. The assignment carries context_type / context_id and an is_primary flag, and is unique per (partner, role, context).
sdk_partner_relationship_types (catalog) and sdk_partner_relationships hold the graph between partners — employment, ownership, a named function. The type carries both a directional_name and a reverse_name, so one row reads correctly from either end.
sdk_partner_profiles holds the many-valued online profiles — website, LinkedIn and the other platforms of its type enum — as type / value / label / sort; value is always a full URL, never a bare handle. (sdk_slot_assignments is not a grant table and has no validity window.)
sdk_partner_addresses and sdk_address_providers are the address domain. Three invariants live in the model rather than the schema, because MySQL cannot express any of them: at most one Primary row per partner (no partial unique index), the structured-or-free-text capture rule, and the coordinate pairing — both columns or neither, and a row with no postal address must carry them (no conditional NOT NULL for either).
Reading the whole thing at once
php artisan migrate:status # what has run
php artisan db:table sdk_partners # one table's real shape in your databaseThe migrations themselves are the authority, and they are commented: each one opens with why the table is shaped the way it is, and which invariants the schema could not carry. If you are about to write a query against an sdk_* table, read its migration first — the comment usually answers the question you were about to ask.
What not to do
- Do not write to a Customizing catalog.
ui5:syncwill delete what you insert. Declare it in code. - Do not insert a grant with a null validity. Use the defaults; nothing in the engine expects a null and the unique keys depend on
valid_until. - Do not write to
sdk_partner_addresseswithDB::table(). You bypass all three model guards. The same caution applies tosdk_partner_relationships. - Do not add columns to an
sdk_*table. They belong to the package and a future migration may touch them. Your own data belongs in your own tables, joined onpartner_id.