Skip to content
Guide

Reference another App record

Store public identities and resolve records with the current actor through the SDK.

Reference another App record

PHP SDK 0.10.12 adds optional ResourceReferenceGroupedResolutionContribution for owners. Preserve each group key, exact as-of date, filters and actor/organization authorization; omit denied references. Existing batch providers keep per-group fallback. Adopt the matching runtime before using this contract. See the public SDK implementation example.

Reference public Resources without importing another App's model or querying its tables. This example links a Workshop Note to a legal-entity-owned Planner Task. Develop both Apps in one project and prepare planner.task and workshop.note through Create a Resource, using table prefixes pln and wsp here.

Reads performed by a reference provider

After the matching Core and Manager runtime upgrade, an ordinary actor-authorized provider can use the existing ResourceReferences methods to validate another public Resource. This works in development sandboxes and installed shared runtimes; it does not expose another App's Eloquent models or database joins. No new SDK method is required.

Core preserves the actor, tenant and exact Legal Entity/Operating Unit context, rechecks each owner, and limits a dependency chain to three provider executions. Nested reads share the original callback's deadline instead of renewing it per hop. Repeated metadata and validated exact/batch results are reused only within the same live read request. Continue to use resolveMany() for a page's records; hasProvider() reports provider presence, not record authorization.

Cycles, excessive depth and exhausted deadlines return RUNTIME_REFERENCE_CYCLE, RUNTIME_REFERENCE_DEPTH_EXCEEDED and RUNTIME_REFERENCE_DEADLINE_EXCEEDED. Delegated Agent and self-service/profile grants retain their existing restricted contracts; an ordinary dependency grant cannot substitute for them. Writes are not authorized by this read capability.

Operators must adopt the matching runtime images, ingress and callback worker configuration before enabling the Core signer. Source changes alone do not establish runtime support or successful application workflows.

Prepare the owner and permissions

Both Apps must be ready in the same sandbox. Assign Task update and Note read permissions with their required legal-entity scopes. Installation prerequisites and App installation do not grant data access.

Current generated Resources have a default reference resolver using their public descriptor and existing viewAny/view policies. You need not create a custom resolver first. Review public integration visibility and the descriptor; do not expose private fields or internal Resources. Custom resolution and historical lookup require additional contracts.

Store the reference identity

Add a new Planner migration. This excerpt belongs in up(); use Data model and migrations for imports, down() and application. Keep the explicit composite-index name and remove that index and the three columns in the reverse migration.

Code example
PHP
Schema::table('pln_tasks', function (Blueprint $table): void {
    $table->string('note_app_key', 64)->nullable();
    $table->string('note_resource_key', 128)->nullable();
    $table->string('note_public_id', 128)->nullable();
    $table->index(['note_app_key', 'note_resource_key', 'note_public_id'], 'pln_task_note_ref');
});

Do not create a foreign key to another App's numeric key. The App key, Resource key and public ID identify the relationship; a cached label is not current resolved data. Supported Core identity FKs are a different contract in Reference Core identities from App tables.

Resolve inside a protected API

Add the imports below to the generated Planner TaskController, then add linkNote() inside the class. Keep its existing guarded route group and Policy.

Code example
PHP
use DateTimeImmutable;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Nexia\Organization\Contracts\OrganizationDirectory;
use Nexia\ResourceReference\Contracts\ResourceReferences;
use Nexia\ResourceReference\ResourceReferenceResolutionContext;
Code example
PHP
public function linkNote(Request $request, string $task): JsonResponse
{
    $record = $this->resolveTask($request, $task, 'planner.task.update');
    $this->authorize('update', $record);
    $data = $request->validate(['note_public_id' => ['required', 'uuid']]);
    $legalEntity = app(OrganizationDirectory::class)
        ->requireLegalEntityByKey($record->legal_entity_id);
    $context = new ResourceReferenceResolutionContext(
        legalEntity: $legalEntity,
        actor: $request->user(),
        asOf: new DateTimeImmutable(),
        purpose: 'planner.task.note',
    );
    $references = app(ResourceReferences::class);
    abort_unless($references->hasProvider('workshop.note'), 422);
    $note = $references->resolve('workshop.note', $data['note_public_id'], $context);
    abort_if($note === null, 422);

    $record->note_app_key = $note->appKey;
    $record->note_resource_key = $note->resourceKey;
    $record->note_public_id = $note->resourceId;
    $record->save();

    return response()->json(['note_reference' => [
        'app_key' => $note->appKey,
        'resource_key' => $note->resourceKey,
        'public_id' => $note->resourceId,
    ]]);
}

Add this route inside the existing authenticated App API group, with the generated TaskController import:

Code example
PHP
Route::post('/tasks/{task}/note', [TaskController::class, 'linkNote'])
    ->whereUuid('task');

Do not accept unrestricted App keys, Resource keys or numeric owner IDs. Build context from the Task's stored owner; the Note owner authorizes its read independently. Do not turn a null resolution, absent provider or permission error into a fabricated saved reference. Resolution and the consumer write are not a distributed transaction; revalidate when finalizing consequential work.

For a picker, use searchPage('workshop.note', $query, $context, page: 1, perPage: 25) with the same authorized context, then submit the selected public ID through the write path above. Resolve again before saving. Search follows the supported owner contract, not arbitrary table lookup.

Verify and handle failures

After both Apps finish nexia dev preparation, link an authorized Task and Note and reload. Resolve the saved identity again for display and show only the currently permitted label. Also check:

  • An unavailable UUID, another legal entity or a denied Note cannot be linked.
  • Revoking Note access or disabling its owner does not leave an old reference displayed as available data.
  • A user without Task update permission is denied even through a direct API call.
  • A stored old label is neither current identity evidence nor authorization.

The default resolver reads current state; do not assume historical or effective-range support. Copying all source data or importing owner code bypasses the intended boundary. Record actor-specific outcomes as described in Test an App.

Exact-self references

A provider may implement ResourceReferenceSelfServiceContribution when an admitted self-service source needs the current actor's records without a directory read grant. authorizesSelfService(context) must accept an explicit set of purposes and require the actor and applicable organization. Every supported resolve, batch, paged and effective-range method must constrain its query to that actor and organization before filtering or pagination. This is an owner promise, not a general permission bypass.

Core restores the originating self-service authority and signs its exact scope; request body flags cannot opt into it. Other providers remain unavailable through this path, and write descriptions and composition operations are excluded. Test forged resource IDs, filters and dates, another actor/organization, unknown purposes, revoked source access and request-scope cleanup. Run these cases through the selected App runtime. Deploy the matching Core, SDK and runtime together before rebuilding adopting Apps.

Public reference query field contract

Every key in ResourceReferenceQuerySchemaContribution::querySchema() under filterable, sortable or searchable must also exist in the same Resource's ResourceDescriptor::fieldSchema. public_id, created_at and name are not added implicitly. This contract describes references exposed to other Apps. A model's ordinary resourceListFields() allowlist does not publish reference fields.

For example, sortable: ['public_id', 'created_at'] and searchable: ['name'] require all three descriptor field schemas, and the provider must implement those conditions. Add only fields that are safe to expose publicly. Remove internal-only fields from public querySchema() and keep them in the App's own list query.

  • Keys must be unique within each list.
  • Conditions require a provider implementing ResourceReferencePagedSearchContribution.
  • A paged provider must also resolve individual records.
  • Schema declarations grant no access; providers must still enforce row and field authorization.

With the corresponding SDK, Core, Manager and CLI updates, missing fields produce query_field_undeclared with a path such as contracts.<resource key>.query.sortable.<field key>. query_duplicate, query_requires_paged, query_requires_resolve and query_invalid identify duplicates, missing paging, missing resolution and malformed declarations. File and line details come from actual exceptions, never guesses from a declaration path.

nexia check inspects source without executing App PHP; it does not prove Core accepted a dynamically generated catalog. Check isolated catalog validation in nexia dev, or pass the actual catalog array to StandardCatalogPolicy::validate($catalog) in an SDK test. Inspect CatalogValidationException::$issues for individual errors.

See the public CLI documentation for command and option details.

Resolve groups by date and filters

PHP SDK 0.10.4 adds the optional GroupedResourceReferences capability, requiring matching Core and App runtime support. Existing ResourceReferences remains supported. Do not assume that every provider supports historical dates or arbitrary filters.

Reuse the authorized $context above and a Resource published by its owning App. Supply public IDs selected and validated for the current task as resource_ids:

Code example
PHP
use Nexia\ResourceReference\Contracts\GroupedResourceReferences;

$references = app(ResourceReferences::class);
if (! $references instanceof GroupedResourceReferences) {
    throw new RuntimeException('Grouped reference resolution is unavailable.');
}
$groups = [
    'current' => ['resource_ids' => $notePublicIds, 'as_of' => new DateTimeImmutable()],
    'previous' => ['resource_ids' => $notePublicIds, 'as_of' => $previousDate],
];
$resolved = $references->resolveManyGrouped('workshop.note', $groups, $context);

Results are keyed by group, then public ID. $previousDate is a business-validated DateTimeImmutable; use it only with a provider supporting historical resolution. Optional filters accept owner-supported fields with string or string-list values. Filters never replace authorization. Missing or denied references may be omitted; compare requested IDs with results and handle missing required records explicitly.

Verify

Compare results at two supported dates and confirm unauthorized IDs and unsupported filters cannot become valid records. For a single date, keep using resolveMany(). SDK 0.10.2 and later validate and deduplicate its IDs and split requests into groups of at most 100.

Read catalog validation issues

PHP SDK 0.10.7 and later expose invalid public reference declarations in CatalogValidationException::$issues, with field, rule, limit, and actual entries. query_field_undeclared identifies a missing public descriptor field; query_duplicate identifies duplicate keys; query_requires_paged and query_requires_resolve identify inconsistent query declarations. Correct the declaration, prepare the App again, and repeat the query. ResourceQueryDiagnostics::issues() provides the same contract checks; it does not automatically publish ordinary model-list fields.

Reference providers may use OpaqueResourceRefs::seal() to encode a link to a resource owned by their App for the current actor. The handle carries identity only: opening it still requires the target resource's read authorization. Provider callbacks cannot use this encoding path to resolve handles or recursively query another App. Adopt the matching Core and Manager runtime fixes before retrying affected reads; no database reset or App SDK API change is required.

Source of truth: docs/developers/content/en/platform-extensions/resource-references.md