Docs: Group API documentation - make "extending" an overview that ingrates more intentionally with its sibling "Record API" and "JS API" docs. #97

This commit is contained in:
Sebastian Jeltsch
2025-07-16 15:18:20 +02:00
parent 9c30ad5f2f
commit a9f3e70da2
8 changed files with 195 additions and 167 deletions
+7 -6
View File
@@ -101,13 +101,17 @@ export default defineConfig({
slug: "documentation/auth",
},
{
label: "APIs",
label: "Endpoints",
items: [
{
slug: "documentation/record_apis",
label: "Overview",
slug: "documentation/apis_overview",
},
{
slug: "documentation/js_apis",
slug: "documentation/apis_record",
},
{
slug: "documentation/apis_js",
},
],
},
@@ -123,9 +127,6 @@ export default defineConfig({
{
slug: "documentation/production",
},
{
slug: "documentation/extending",
},
],
},
{
@@ -0,0 +1,57 @@
---
title: JS/TS APIs
---
import { Aside } from "@astrojs/starlight/components";
On startup TrailBase will automatically load any JavaScript and TypeScript
files in `traildepot/scripts`.
This can be used to implement arbitrary HTTP APIs using custom handlers.
## Example HTTP Endpoint
The following example illustrates a few things:
* How to register a parameterized route with `{table}`.
* How to implement a handler that returns `text/plain` content. There is also
`jsonHandler` and `htmlHandler`.
* How to query the database.
* How to return an error.
```js
import {
addRoute,
query,
stringHandler,
HttpError,
StatusCodes
} from "../trailbase.js";
addRoute("GET", "/test/{table}", stringHandler(async (req) => {
const table = req.params["table"];
if (table) {
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
return `entries: ${rows[0][0]}`;
}
throw new HttpError(
StatusCodes.BAD_REQUEST, "Missing '?table=' search query param");
}));
```
More examples can be found in the repository in
`client/testfixture/scripts/index.ts`.
## Runtime Details
At its heart, TrailBase's runtime is a pool of V8 worker threads - called
*isolates* - alongside a runtime that supports basic tasks such as file I/O, web
requests, timers, etc.
While it uses Deno's V8-integration under the hood, it is *not* a full Node.js
runtime, at least for now.
<Aside type="note" title="State Sharing">
Different *isolates* do not share state, i.e. you cannot use global state to
reliably share state across requests. Instead, state can be persisted using
the database.
</Aside>
@@ -0,0 +1,129 @@
---
title: Endpoints
description: Collocating your custom business logic
---
import { Aside } from "@astrojs/starlight/components";
When building any connected application, eventually you'll want to call an API
endpoint:
1. to [read/write some data](#data-apis), or
2. to run some [server-side application logic](#custom-endpoints-inside-trailbase).
Especially for application logic, where and how it should run depends on your
app, your environment, and external requirements.
For example, for rich client-side applications - such as mobile, desktop, and
progressive web apps - it is often a good idea to run inside the user's device.
It is cheap, snappy, privacy-friendly and able to run offline or on a spotty
connection.
That said, there are very valid reasons to not run everything in an untrusted,
resource and battery-limited, hard to update sandbox.
## Data APIs
Data APIs are limited APIs that let you perform a common operations on your
data: **read/list**, **create**, **update** and **delete**.
In the simplest case, you can use TrailBase's [record
APIs](/documentation/apis_record/) to define limited-access APIs over your
`TABLE`s and `VIEW`s.
If you require more flexibility, the following provides an overview of ways to
run arbitrary logic.
## Custom Endpoints inside TrailBase
There are several ways to integrate custom logic into TrailBase, however you
can also run logic outside in your [own backend](#bring-your-own-backend).
Within TrailBase you have the following options:
1. [JS & TS handlers](#javascript--typescript-handlers),
2. [Stored database procedures](#stored-procedures),
3. [Native Rust handlers](#rust-handlers).
### JavaScript & TypeScript Handlers
TrailBase's built-in runtime enables wiring custom HTTP endpoints using full
ES6 JavaScript and/or TypeScript.
For more context, check out the dedicated [docs](/documentation/apis_js/).
### Stored Procedures
Unlike Postgres or MySQL, SQLite does not support stored procedures out of the
box.
TrailBase makes Sqlean's
[user-defined functions](https://github.com/nalgeon/sqlean/blob/main/docs/define.md)
available to fill the gap.
They can be accessed from record APIs through `VIEW`s or custom handlers.
<Aside type="note" title="Portability">
Sqlean can be used with any SQLite client, thus avoiding lock-in.
</Aside>
{/* Is this too obtuse to actually be useful?
### SQLite Extensions and Virtual Tables
Likely the most bespoke approach is to expose your functionality as a custom
SQLite extension or module similar to how TrailBase extends SQLite itself.
This approach can be somewhat limiting in terms of dependencies you have
access to and things you can do especially for extensions. Modules are quite a bit
more flexible but also involved.
Take a look at [SQLite's list](https://www.sqlite.org/vtablist.html) and
[osquery](https://osquery.readthedocs.io/en/stable/) to get a sense of what's
possible.
Besides their limitations, major advantages of using extensions or
modules are:
* you have extremely low-overhead access to your data,
* extensions and modules can also be used by services accessing the
underlying SQLite databases.
*/}
### Rust Handlers
The Rust APIs aren't yet stable and fairly undocumented.
That said, similar to using PocketBase as a Go framework, you can build your
own TrailBase binary and register custom
[axum](https://github.com/tokio-rs/axum) HTTP handlers written in rust with the
main application router, see `/examples/custom-binary`.
<Aside type="note" title="API Stability">
the Rust APIs are subject to change. However, we will rely on semantic
versioning to communicate breaking changes explicitly.
</Aside>
## Bring your own Backend
The most flexible and de-coupled way of running your own code is to deploy a
separate service in front of or alongside TrailBase.
This gives you full control over your destiny: language, runtime, scaling,
deployment, etc.
TrailBase is designed with the explicit goal of running along a sea of other
services.
Its stateless tokens using asymmetric crypto make it easy for other resource
servers to hermetically authenticate your users.
TrailBase's APIs can be accessed transitively, simply by forwarding users'
[auth tokens](/documentation/auth/) [^1].
Alternatively, for more of a side-car setup you can fall back to accessing the
SQLite database directly, both for data access and schema alterations[^2].
---
[^1]:
We would like to add service accounts in the future to authorize privileged
services independent from user-provided tokens or using fake user-accounts
for services.
[^2]:
SQLite is running in WAL mode, which allows for parallel reads and
concurrent writes. That said, when possible you should probably use the APIs
since falling back to raw database access is a priviledge practically reserved
to processes with access to a shared file-system.
@@ -1,105 +0,0 @@
---
title: Custom Logic
description: Collocating your custom business logic
---
import { Aside } from "@astrojs/starlight/components";
This article explores different ways to integrate your App with TrailBase,
extend it and your custom logic.
The question of where code should run weighs heavily on the web: push
everything to the server, more to the client or even the edge?
Answering this question is s a lot simpler for rich client-side applications
such as mobile, desktop, and progressive web apps or SPAs where the inclination
is to run on the users device providing privacy friendly, snappy interactivity
and offline capabilities.
There are perfectly good reasons to not run everything in an untrusted, battery
limited, SEO unfriendly client-side sandbox but the overall need for
server-side execution is greatly reduced.
**It's rich client-side apps where application servers like TrailBase can shine
providing common server-side functionality and strategic extension points**.
## Bring your own Backend
The most flexible and likewise de-coupled way of running your own code is to
deploy a separate service alongside TrailBase. This gives you full control over
your destiny: runtime, scaling, deployment, etc.
TrailBase is designed with the explicit goal of running along a sea of other
services.
Its stateless tokens using asymmetric crypto make it easy for other resource
servers to hermetically authenticate your users.
TrailBase's APIs can be accessed transitively, simply by forwarding user
tokens [^1].
Alternatively, you can fall back to raw SQLite for reads, writes and even
schema alterations[^2].
## Custom APIs in TrailBase
TrailBase provides a couple of ways to embed custom logic and provide custom APIs endpoints:
1. Rust HTTP handlers using Axum,
2. JS/TS handlers [APIs](/documentation/js_apis/),
3. Stored database procedures,
3. SQLite extensions and modules (virtual tables).
<Aside type="note" title="Rust Handlers">
the Rust APIs are subject to change. However, we will rely on semantic
versioning to communicate breaking changes explicitly.
</Aside>
### Using ES6 JavaScript & TypeScript
You can write custom HTTP endpoints using both full ES6 JavaScript and/or
TypeScript. TrailBase will transpile your code on the fly and execute it on a
speedy V8-engine, the same engine found across Chrome, node.js and deno.
More information can be found in the [API docs](/documentation/js_apis/).
### Using Rust
The Rust APIs aren't yet stable and fairly undocumented.
That said, similar to using PocketBase as a Go framework, you can build your
own TrailBase binary and register custom Axum handlers written in rust with the
main application router, see `/examples/custom-binary`.
### Stored Procedures
Unlike Postgres or MySQL, SQLite does not support stored procedures out of the
box.
However, TrailBase has integrated sqlean's
[user-defined functions](https://github.com/nalgeon/sqlean/blob/main/docs/define.md)
to fill the gap. You can easily adopt SQLean in your own backends avoiding
lock-in.
### SQLite Extensions and Modules a.k.a. Virtual Tables
Likely the most bespoke approach is to expose your functionality as a custom
SQLite extension or module similar to how TrailBase extends SQLite itself.
This approach can be somewhat limiting in terms of dependencies you have
access to and things you can do especially for extensions. Modules are quite a bit
more flexible but also involved.
Take a look at [SQLite's list](https://www.sqlite.org/vtablist.html) and
[osquery](https://osquery.readthedocs.io/en/stable/) to get a sense of what's
possible.
Besides their limitations, major advantages of using extensions or
modules are:
* you have extremely low-overhead access to your data,
* extensions and modules can also be used by services accessing the
underlying SQLite databases.
---
[^1]:
We would like to add service accounts in the future to authorize privileged
services independent from user-provided tokens or using fake user-accounts
for services.
[^2]:
SQLite is running in WAL mode, which allows for parallel reads and
concurrent writes. That said, when possible you should probably use the APIs
since falling back to raw database access is a priviledge practically reserved
to processes with access to a shared file-system.
@@ -1,55 +0,0 @@
---
title: JS/TS APIs
---
import { Aside } from "@astrojs/starlight/components";
You can place JavaScript and TypeScript into `traildepot/scripts` and TrailBase
will automatically load them on startup.
For now we support custom HTTP handlers letting you register routes, act on
requests, query the database and build arbitrary responses.
## Runtime
Before we jump into details, let's quickly talk about the runtime itself. At
its heart, it's a pool of V8-js-engines alongside a runtime that supports basic
tasks such as file I/O, web requests, timers, etc.
However, it is *not* a complete Node.js runtime, at least for now, since it
would pull in a lot of extra dependencies.
Note further, that the pool of workers/isolates does not share state, i.e. you
cannot use global state to reliably share state across requests. You should
rely on the database for persisting and sharing state.
## HTTP Endpoints
The following example illustrates a few things:
* How to register a parameterized route with `:table`.
* How to implement a handler that returns `text/plain` content. There is also
`jsonHandler` and `htmlHandler`.
* How to query the database.
* How to return an error.
```js
import {
addRoute,
query,
stringHandler,
HttpError,
StatusCodes
} from "../trailbase.js";
addRoute("GET", "/test/{table}", stringHandler(async (req) => {
const table = req.params["table"];
if (table) {
const rows = await query(`SELECT COUNT(*) FROM ${table}`, [])
return `entries: ${rows[0][0]}`;
}
throw new HttpError(
StatusCodes.BAD_REQUEST, "Missing '?table=' search query parm");
}));
```
More examples can be found in the repository in
`client/testfixture/scripts/index.ts`.
@@ -69,7 +69,7 @@ export interface Article {
TrailBase also supports generating type-safe bindings for columns containing
JSON data and enforcing a specific JSON schema, see
[here](/documentation/record_apis/#custom-json-schemas).
[here](/documentation/apis_record/#custom-json-schemas).
---