The starter
The CLI’sinit command creates the app from the starter bundled with the CLI, with Tailwind and shadcn/ui set up:
app/routes/home.tsxis the first screen. The starter’s version is a small notes page that reads and writes thenotesdatabase.app/root.tsxre-exports the SDK’s documentLayout, which mounts the chrome on the hosted slate. Keep this re-export when you edit the root. The root’s error boundary shows setup guidance when a connection has no assigned account or a viewer-mode secret has no value.public/slate.jsonis the app’s declaration.databases/notes/holds thenotesdatabase’s Kysely configuration, migrations and generated query types.
Declare resources
The app names the resources it requires inpublic/slate.json. ContextLayer uses this declaration to prepare the app’s resource capabilities; the file does not contain account credentials, secret values, or database contents. The starter declares one database:
public/slate.json
connections is required, even when empty. Here, notes is the database slug that the app uses in queries, and "mode": "owner" selects the slate Owner’s database for all viewers. A resource declared with "mode": "viewer" instead selects the current viewer’s resource. Resources explains the declaration fields and ownership modes.
Run locally
From the checkout root,pnpm dev starts the development server; open the local URL it prints. Locally, the app uses your own resources:
- Connections use your personal accounts, whatever their declared mode. Assign them with the CLI’s
connectionscommand (npx @usecontextlayer/cli connections --help). Local assignments are saved in.contextlayer/connections.json. - Secrets come from environment variables with the declared names.
- Databases are stored in
.contextlayer/databases.
pnpm dev after changing declarations, assignments, or secret values so startup can prepare the new configuration. When the app declares connections, the development server also needs your CLI login. The account connection guide walks through local assignments.
Run pnpm typecheck to check the checkout’s TypeScript and pnpm build to build it for production. Local database contents and environment values are not uploaded when you deploy.
Use connected services and databases
The SDK supplies resource capabilities through React Router’s request context. A loader or action asks for the capability it needs and selects a resource by its declared slug:context.get(toolsContext)returns Tools.tools.call(slug, tool, arguments)runs a tool through one of the app’s declared connections and resolves to{ data, logId }. Validatedatabefore using it.database<DB>(context.get(databasesContext), slug)returns a typed Kysely instance for a declared database.context.get(databasesContext).query(slug, sql, params)runs SQL directly.
Query the notes database
This excerpt from the notes route shows the loader reading notes newest first and the action inserting a submitted note. Both select the declarednotes database; the action validates the form value before inserting it. Keep the route’s rendered component alongside these server functions:
app/routes/home.tsx
"notes" argument to database() is a database slug. The "notes" argument to selectFrom() or insertInto() is a table name inside that database. They happen to match in the starter; they are separate names.
Call a connected service
A connected-service call follows the same pattern. This excerpt assumes you have declared a Gmail connection namedwork-email in public/slate.json and assigned an account to it. Inside a route loader or action, obtain Tools and name that connection:
work-email selects the declared connection, GMAIL_FETCH_EMAILS selects the provider tool, and the object supplies that tool’s arguments. data is the provider result; validate its shape before using it. logId identifies the tool execution. See the SDK reference for the call contract.
Keep these calls in loaders and actions. Browser components submit requests to those routes and receive the data they return; they receive no ContextLayer credentials. On the hosted slate, each resource call is bound to the slate and the viewer making the request. The declaration controls which resources are available, and your route code controls which operations the interface exposes.
When a declared connection has no assigned account, or a viewer-mode secret has no value, the call throws an HTTP 409 response. The starter’s root error boundary turns it into setup guidance; the SDK’s connectionRequiredSchema and secretRequiredSchema recognize it in a custom error boundary. The SDK reference documents the full API.
Deploy
Commit your changes and pushmain to the slate’s remote. See Deploy with a Git push.