Skip to content
Guide

Reference Core identities from App tables

Choose a supported Core foreign key, define the migration, and keep data access authorized.

Reference Core identities from App tables

An App can reference selected Core identity keys with a database foreign key. This enforces existence; it does not grant permission to read Core tables or use a particular record. Start with an App running through Quickstart. Paths below are relative to the App root.

Choose the reference

TargetReferenced columnPostgreSQL typeMeaning
public.legal_entitiesidbigintLegal entity
public.operating_unitsidbigintOperating unit
public.partiesidbigintPerson or organization identity
public.sitesidbigintSite
public.usersidbigintTenant user

Only these columns are in the current Core FK catalog. Their public_id UUIDs are external identifiers, not granted FK targets. A Party is not an employee record, and a User is not every person. Do not add cross-App table FKs or infer that another Core table is available.

RelationshipStore and use
Another table owned by this AppAn App-owned FK and index
One of the Core identities aboveThe supported numeric key, resolved on the backend through the appropriate SDK contract
Another App's business recordIts stable App key, Resource key and public record identifier; use an authorized Resource Reference, not a table join

The platform grants the App migrator REFERENCES; it does not grant SELECT, INSERT, UPDATE, or DELETE on Core tables. Both Composer and isolated App runtimes follow this catalog. Developers do not request DB credentials or run GRANT. The current public CLI does not expose nexia db references or nexia db reset.

Add the migration

This example adds an optional site to the Workshop Note table. Use the actual table prefix in your generated App. Do not duplicate its existing ownership column. Create a new file after the Note creation migration, for example database/migrations/tenant/2026_10_04_120000_add_site_to_wsp_notes.php:

Code example
PHP
<?php

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('wsp_notes', function (Blueprint $table): void {
            $table->unsignedBigInteger('site_id')->nullable();
            $table->index('site_id');
            $table->foreign('site_id')->references('id')
                ->on('public.sites')->restrictOnDelete();
        });
    }

    public function down(): void
    {
        Schema::table('wsp_notes', function (Blueprint $table): void {
            $table->dropForeign(['site_id']);
            $table->dropIndex(['site_id']);
            $table->dropColumn('site_id');
        });
    }
};

The App table uses its allocated schema; qualify only the Core target with public. Do not create the App table in public or hard-code an allocated schema name. The platform does not create an FK or its referencing index merely because a target is allowed. Choose deletion behavior deliberately; this example blocks deletion of a referenced site. down() discards the column's data and is not a routine recovery command.

Resolve and authorize before saving

Accept a public identifier at the API boundary. Preserve the generated Resource Policy and organization-target checks, then resolve the site for the authorized legal entity. This excerpt belongs in an already authorized controller operation; $legalEntityKey must come from its trusted resolved owner, never a numeric request field:

Code example
PHP
use Nexia\Organization\Contracts\SiteDirectory;

$data = $request->validate(['site_public_id' => ['required', 'uuid']]);
$site = app(SiteDirectory::class)->findByPublicIdForLegalEntity(
    $data['site_public_id'],
    $legalEntityKey,
);
abort_if($site === null, 422);
$record->site_id = $site->key;
$record->save();

This only shows a non-null assignment. For an optional field, separately handle omission (preserve the value) and explicit null (clear it). Hide site_id in the model's serialized output and return the permitted public identifier instead. Update form and response types as described in Data model and migrations.

Use OrganizationDirectory for legal entities and operating units, SiteDirectory for sites, and Nexia\Laravel\Identity\Contracts\PartyDirectory for Party identities. Identity resolution does not replace permission or business-eligibility checks. Do not query public.sites, import Core Eloquent models, or use exists:sites,id as an authorization check. An FK alone does not enforce legal-entity affiliation, active status, soft deletion or the caller's permissions.

Apply and verify

Let nexia dev finish synchronizing the new migration, then stop that watcher with Ctrl+C. In the App folder:

Code example
Shell
nexia db migrate
nexia db status

Wait for completed, then restart nexia dev. Confirm an authorized assignment survives reload, an unavailable or out-of-scope site is rejected, and existing Notes with no site remain editable. Use separate test data when testing constraint failures.

SymptomNext action
Permission denied on a Core FK targetCheck the exact table/column above and that this is an App migration. If it still fails, send the operation ID to support; do not widen grants.
Type mismatchCore id is bigint; do not store a UUID in that FK column.
FK violationResolve the identity in this tenant and handle deletion/concurrent change; do not remove the constraint to accept invalid input.
queued, running, or needs_reviewFollow Data model and migrations; accepted is not completed and reset is not recovery.
Source of truth: docs/developers/content/en/building-apps/core-database-references.md