Skip to content
Backlit
Esc
navigateopen⌘Jpreview
On this page

Introduction

What Backlit is, the work it removes, and the packages it ships.

Backlit is a server-driven UI (SDUI) framework for admin panels and internal apps. You declare the data and the interface together on the server. A web client renders the result.

Do not build another app

An admin panel is usually a second application that you build next to your product. It starts with a data-only API, because the API of the product is scoped to one customer, and an admin panel needs elevated privileges and data across customers. Then you build an interface, and then the client code that connects the interface to the API. The interface is usually the only consumer that the API will ever have.

Backlit is SDUI, and SDUI collapses the three pieces into one. A resource on your server reads the data and declares the UI primitives that display it, in the same file. There is no separate API to build, and there is no client code to write. The web client renders the response. The SDUI primer covers the background of this architecture and how it works in Backlit.

The three steps below build a page that lists the subscribers of a newsletter.

Define a resource with fields

A resource is a named entity of your domain, such as a subscriber or an order. It holds the fields of the entity and the views that display it.

A field describes one piece of data: its name, its data type, its format, and how the client renders it. You declare a field one time. Tables, datalists, and forms all accept the same declaration, so a label or a format is never written twice.

A field is not a mapping to a database column, although many fields have a column with the same name. The data of a field can come from anywhere. A field can also describe a computed value, an aggregate, or a statistic, such as the open rate of a newsletter. The fields reference lists the field types and their options.

import { defineResource, fields } from '@backlit/sdk'

const subscribers = defineResource('subscribers', [
  fields.text('email', { label: 'Email', format: 'email' }),
  fields.text('name', { label: 'Name' }),
  fields.enum('status', ['pending', 'subscribed', 'unsubscribed'], {
    label: 'Status',
    // The client draws the value as a badge and not as plain text.
    displayVariant: 'badge',
  }),
  fields.date('subscribedAt', { label: 'Subscribed', displayVariant: 'datetime' }),
])

export default subscribers

Define a view

A view is one screen of a resource. Its address is the resource name and the view name, thus subscribers/list here. The callback runs on your server for each request. It reads the data through your own data layer and returns the blocks that display the data.

This view returns a section block that contains a table block. The table gets its columns from the fields of the resource, selected with subscribers.pick.

import { defineResource, fields, section, table } from '@backlit/sdk'

import { db } from '../db.ts'

// ...the resource from the previous step

subscribers.view('list', async (ctx, view) => {
  view.setTitle('Subscribers')

  // Your query. It runs on the server with the privileges of your admin area.
  const rows = await db.subscribers.findAll()

  return section('Subscribers').setContent([
    table(rows).setFields(subscribers.pick('name', 'email', 'status', 'subscribedAt')),
  ])
})

Create the app and add the view to the sidebar

The app is the root of the declaration. It groups resources into areas, and each area has a navigation list for the sidebar. A navigation entry names a resource and one of its views. The resources are lazy imports, so a request loads only the resource that it needs.

import { defineApp } from '@backlit/sdk'

export const app = defineApp({
  areas: {
    main: {
      label: 'Newsletter',
      resources: {
        subscribers: () => import('./resources/subscribers.ts'),
      },
      navigation: [
        { label: 'Subscribers', resource: 'subscribers', view: 'list', icon: 'users' },
      ],
    },
  },
})

A host adapter mounts the app on your HTTP server. Backlit does not own the server, the authentication, or the database. Your middleware runs before the Backlit routes, as it does for your other routes.

import { Hono } from 'hono'
import { backlitRoutes } from '@backlit/hono'

import { app } from './app.ts'

const server = new Hono()
server.route('/api', backlitRoutes(app))

The web client requests the subscribers/list view, receives a tree of protocol nodes as JSON, and renders the page.

One protocol, many SDKs and clients

The center of Backlit is a protocol, not a library. The protocol defines the nodes that a server can send and the requests that a client can make. An SDK is a program that emits the protocol. A client is a program that renders it.

Backlit ships a TypeScript SDK and a web client today. Because the two sides share only the protocol, an SDK for a different language and a client for a mobile platform can be added without a change to the other side. Both are planned.

Packages

Package Runs on Purpose
@backlit/sdk Server A TypeScript library that works on any runtime. Its builders declare resources, views, actions, and blocks, and they emit protocol responses.
@backlit/ui Browser The web client. It is a React library that requests views, renders protocol nodes, and submits actions.
@backlit/hono Server The host adapter for Hono. It mounts the routes of an app and maps requests and responses.
@backlit/contract Both The protocol as TypeScript types. The SDK and the web client both depend on it. An extension author uses it to add a node.

Next steps

Was this page helpful?