Skip to content

Remote components

Remote lets one Lattice application embed a component whose data — or whole schema — comes from a different application. A page renders a remote.data-list (or a remote-configured chat box); the browser exchanges a signed reference for a short-lived, audience-scoped token and calls the remote service directly. The remote service stays in control of what it hands out, and the embedding app never proxies the data.

RoleResponsibility
Consumer (the Lattice backend)Renders the page and embeds the remote component. Hosts the token endpoint that mints browser tokens for its logged-in user.
Remote serviceServes the data endpoint (and optionally the schema manifest) that the browser calls with the issued token.

A single application is often both — it embeds its own remote sources during development, which is exactly what the workbench does.

 
Browser-token exchange for a remote component

The Lattice backend seals a tamper-proof ref that binds the node identity to a source, audience, and scopes. The browser sends that ref (as a header, with the session cookie) back to the Lattice backend’s token endpoint, which re-checks every bound value before asking the matching source to issue a token. The call to the remote service carries only the resulting bearer token — credentials are omitted, so no cookies leak across the origin boundary.

DataList (registered as remote.data-list) fetches rows from a remote endpoint and renders a reusable row schema for each one. Configure the source, the audience it is for, the scopes it needs, and the remote service’s data endpoint:

use Lattice\Lattice\Remote\Components\DataList;
use Lattice\Lattice\Core\Components\Card;
use Lattice\Lattice\Core\Components\Text;
DataList::make('todos')
->source('workbench.todos')
->audience('https://todos.example.test')
->scopes(['todos.read'])
->dataEndpoint('https://todos.example.test/api/todos')
->emptyLabel('No todos yet')
->schema([
Card::make()->dataBindings(['title' => 'title', 'description' => 'detail']),
]);

Each row in the remote service’s { "data": [...] } payload is materialized through the child schema. dataBindings map a component prop to a row key (dotted paths are supported), so the same row schema renders every record.

A chat box can also read from a remote source. There is no remote.chat-box type — the regular chat.box component extends the remote machinery, so you opt in by calling source()/audience() on it:

use Lattice\Lattice\Chat\Components\ChatBox;
ChatBox::make('assistant')
->source('workbench.todos')
->audience('https://todos.example.test')
->scopes(['chat.read', 'chat.write'])
->streamEndpoint('https://todos.example.test/api/chat/stream')
->historyEndpoint('https://todos.example.test/api/chat/history')
->fill();

A source describes a remote service. Extend RemoteSourceDefinition, tag it with #[AsRemoteSource], and override issueBrowserToken() to mint a token after the consumer’s endpoint has verified the request:

use Illuminate\Http\Request;
use Lattice\Lattice\Attributes\AsRemoteSource;
use Lattice\Lattice\Remote\BrowserToken;
use Lattice\Lattice\Remote\RemoteSourceDefinition;
#[AsRemoteSource('workbench.todos')]
final class TodoSource extends RemoteSourceDefinition
{
public function issueBrowserToken(Request $request): BrowserToken
{
return new BrowserToken(
accessToken: '', // call your real authorization server here
tokenType: 'Bearer',
expiresIn: 120,
audience: $request->string('audience')->toString(),
scopes: $request->array('scopes'),
);
}
}

Sources are discovered like every other definition, or you can register them explicitly:

use Lattice\Lattice\Facades\Lattice;
Lattice::remoteSources([\App\Remote\TodoSource::class]);
// Resolve keys dynamically — e.g. one source per tenant:
Lattice::remoteSourceResolver(
fn (string $key, $container) => str_starts_with($key, 'tenant:')
? new TenantSource(/**/)
: null,
);

Override authorize(Request) on the definition to gate token issuance per user; it defaults to allowing the request.

A source can also hand the consumer an entire schema manifest to render, not just rows. Override schemaEndpoint() to point at a JSON manifest — an allow-listed URL or a local file — and return its nodes from schema():

use Lattice\Lattice\Remote\RemoteSchemaEndpoint;
public function schemaEndpoint(Request $request): RemoteSchemaEndpoint
{
return RemoteSchemaEndpoint::url(
'https://todos.example.test/lattice/manifest',
allowedHosts: ['todos.example.test'],
);
// …or RemoteSchemaEndpoint::file(storage_path('manifests/todos.json'));
}

Render it by resolving the source on a page:

public function render(PageSchema $schema, Request $request, RemoteSourceRegistry $remoteSources): PageSchema
{
return $schema->schema(
$remoteSources->resolve('workbench.todos')->schema($request),
);
}

The manifest is a tree of { "type", "id", "props", "schema" } nodes. A remote-capable node only needs to declare its audience and scopes — the consumer stamps the trusted remote access:

{
"type": "remote.data-list",
"id": "todos",
"props": {
"dataEndpoint": "/api/todos",
"audience": "https://todos.example.test",
"scopes": ["todos.read"]
},
"schema": [{ "type": "card", "props": { "dataBindings": { "title": "title" } } }]
}

The remote.data-list renderer ships in a separate plugin that you add to your registry:

import { createRegistry } from "@lattice-php/lattice/core/registry";
import { remoteComponents } from "@lattice-php/lattice/remote";
export const registry = createRegistry(/* …core plugins…, */ remoteComponents);

A remote chat box needs chatPlugin instead — it registers the chat.box type that remote chat reuses. Unlike chatPlugin, remoteComponents is not re-exported from the package root, so import it from the @lattice-php/lattice/remote subpath as shown above.

The token endpoint and its middleware live under remote-sources in config/lattice.php:

'remote-sources' => [
'endpoint' => 'lattice/remote-sources/{source}/token',
'middleware' => ['web', 'auth'],
],

{source} is URL-encoded into the path, so the wire tokenEndpoint for a source keyed workbench.todos is /lattice/remote-sources/workbench.todos/token. The signed ref lifetime is shared with the rest of Lattice via security.ref_lifetime.

See Configuration for the full endpoint table and Security for how component references are signed and verified.