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.
Two roles
Section titled “Two roles”| Role | Responsibility |
|---|---|
| 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 service | Serves 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.
How a request flows
Section titled “How a request flows”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.
Embedding a data list
Section titled “Embedding a data list”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.
Remote chat
Section titled “Remote chat”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();Remote sources
Section titled “Remote sources”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.
Schema-driven federation
Section titled “Schema-driven federation”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" } } }]}Frontend setup
Section titled “Frontend setup”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.
Configuration
Section titled “Configuration”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.