Merge pull request #102 from bknd-io/feat/aws-lambda-adapter

added aws lambda adapter + improvements to handle concurrency
This commit is contained in:
dswbx
2025-03-03 16:59:16 +01:00
committed by GitHub
33 changed files with 4460 additions and 1444 deletions

View File

@@ -216,6 +216,7 @@ async function buildAdapters() {
await tsup.build(baseConfig("remix")); await tsup.build(baseConfig("remix"));
await tsup.build(baseConfig("bun")); await tsup.build(baseConfig("bun"));
await tsup.build(baseConfig("astro")); await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build( await tsup.build(
baseConfig("cloudflare", { baseConfig("cloudflare", {
external: [/^kysely/], external: [/^kysely/],

View File

@@ -3,7 +3,7 @@
"type": "module", "type": "module",
"sideEffects": false, "sideEffects": false,
"bin": "./dist/cli/index.js", "bin": "./dist/cli/index.js",
"version": "0.9.0-rc.1-7", "version": "0.9.0-rc.1-11",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.", "description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, Remix, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io", "homepage": "https://bknd.io",
"repository": { "repository": {
@@ -191,6 +191,11 @@
"import": "./dist/adapter/astro/index.js", "import": "./dist/adapter/astro/index.js",
"require": "./dist/adapter/astro/index.cjs" "require": "./dist/adapter/astro/index.cjs"
}, },
"./adapter/aws": {
"types": "./dist/types/adapter/aws/index.d.ts",
"import": "./dist/adapter/aws/index.js",
"require": "./dist/adapter/aws/index.cjs"
},
"./dist/main.css": "./dist/ui/main.css", "./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css", "./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json" "./dist/manifest.json": "./dist/static/.vite/manifest.json"

View File

@@ -58,6 +58,8 @@ export class App {
adminController?: AdminController; adminController?: AdminController;
private trigger_first_boot = false; private trigger_first_boot = false;
private plugins: AppPlugin[]; private plugins: AppPlugin[];
private _id: string = crypto.randomUUID();
private _building: boolean = false;
constructor( constructor(
private connection: Connection, private connection: Connection,
@@ -90,6 +92,11 @@ export class App {
server.use(async (c, next) => { server.use(async (c, next) => {
c.set("app", this); c.set("app", this);
await next(); await next();
try {
// gracefully add the app id
c.res.headers.set("X-bknd-id", this._id);
} catch (e) {}
}); });
}, },
}); });
@@ -100,9 +107,18 @@ export class App {
return this.modules.ctx().emgr; return this.modules.ctx().emgr;
} }
async build(options?: { sync?: boolean }) { async build(options?: { sync?: boolean; fetch?: boolean; forceBuild?: boolean }) {
// prevent multiple concurrent builds
if (this._building) {
while (this._building) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
if (!options?.forceBuild) return;
}
this._building = true;
if (options?.sync) this.modules.ctx().flags.sync_required = true; if (options?.sync) this.modules.ctx().flags.sync_required = true;
await this.modules.build(); await this.modules.build({ fetch: options?.fetch });
const { guard, server } = this.modules.ctx(); const { guard, server } = this.modules.ctx();
@@ -127,6 +143,8 @@ export class App {
app: this, app: this,
}); });
} }
this._building = false;
} }
mutateConfig<Module extends keyof Modules>(module: Module) { mutateConfig<Module extends keyof Modules>(module: Module) {

View File

@@ -0,0 +1,68 @@
import type { App } from "bknd";
import { handle } from "hono/aws-lambda";
import { type RuntimeBkndConfig, createRuntimeApp } from "bknd/adapter";
export type AwsLambdaBkndConfig = RuntimeBkndConfig & {
assets?:
| {
mode: "local";
root: string;
}
| {
mode: "url";
url: string;
};
};
let app: App;
export async function createApp({
adminOptions = false,
assets,
...config
}: AwsLambdaBkndConfig = {}) {
if (!app) {
let additional: Partial<RuntimeBkndConfig> = {
adminOptions,
};
if (assets?.mode) {
switch (assets.mode) {
case "local":
// @todo: serve static outside app context
additional = {
adminOptions: adminOptions === false ? undefined : adminOptions,
serveStatic: (await import("@hono/node-server/serve-static")).serveStatic({
root: assets.root,
onFound: (path, c) => {
c.res.headers.set("Cache-Control", "public, max-age=31536000");
},
}),
};
break;
case "url":
additional.adminOptions = {
...(typeof adminOptions === "object" ? adminOptions : {}),
assets_path: assets.url,
};
break;
default:
throw new Error("Invalid assets mode");
}
}
app = await createRuntimeApp({
...config,
...additional,
});
}
return app;
}
export function serveLambda(config: AwsLambdaBkndConfig = {}) {
console.log("serving lambda");
return async (event) => {
const app = await createApp(config);
return await handle(app.server)(event);
};
}

View File

@@ -0,0 +1 @@
export * from "./aws-lambda.adapter";

View File

@@ -34,6 +34,7 @@ export function serve({
port = config.server.default_port, port = config.server.default_port,
onBuilt, onBuilt,
buildConfig, buildConfig,
adminOptions,
...serveOptions ...serveOptions
}: BunBkndConfig = {}) { }: BunBkndConfig = {}) {
Bun.serve({ Bun.serve({
@@ -46,6 +47,7 @@ export function serve({
options, options,
onBuilt, onBuilt,
buildConfig, buildConfig,
adminOptions,
distPath, distPath,
}); });
return app.fetch(request); return app.fetch(request);

View File

@@ -14,6 +14,8 @@ export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & { export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
distPath?: string; distPath?: string;
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
}; };
export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig { export function makeConfig<Args = any>(config: BkndConfig<Args>, args?: Args): CreateAppConfig {
@@ -55,14 +57,7 @@ export async function createFrameworkApp<Args = any>(
} }
export async function createRuntimeApp<Env = any>( export async function createRuntimeApp<Env = any>(
{ { serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
serveStatic,
adminOptions,
...config
}: RuntimeBkndConfig & {
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
},
env?: Env, env?: Env,
): Promise<App> { ): Promise<App> {
const app = App.create(makeConfig(config, env)); const app = App.create(makeConfig(config, env));

View File

@@ -0,0 +1,36 @@
import { getRelativeDistPath } from "cli/utils/sys";
import type { CliCommand } from "../types";
import { Option } from "commander";
import fs from "node:fs/promises";
import path from "node:path";
import c from "picocolors";
export const copyAssets: CliCommand = (program) => {
program
.command("copy-assets")
.description("copy static assets")
.addOption(new Option("-o --out <directory>", "directory to copy to"))
.addOption(new Option("-c --clean", "clean the output directory"))
.action(action);
};
async function action(options: { out?: string; clean?: boolean }) {
const out = options.out ?? "static";
// clean "out" directory
if (options.clean) {
await fs.rm(out, { recursive: true, force: true });
}
// recursively copy from src/assets to out using node fs
const from = path.resolve(getRelativeDistPath(), "static");
await fs.cp(from, out, { recursive: true });
// in out, move ".vite/manifest.json" to "manifest.json"
await fs.rename(path.resolve(out, ".vite/manifest.json"), path.resolve(out, "manifest.json"));
// delete ".vite" directory in out
await fs.rm(path.resolve(out, ".vite"), { recursive: true });
console.log(c.green(`Assets copied to: ${c.bold(out)}`));
}

View File

@@ -19,6 +19,7 @@ const config = {
node: "Node.js", node: "Node.js",
bun: "Bun", bun: "Bun",
cloudflare: "Cloudflare", cloudflare: "Cloudflare",
aws: "AWS Lambda",
}, },
framework: { framework: {
nextjs: "Next.js", nextjs: "Next.js",

View File

@@ -4,3 +4,4 @@ export { run } from "./run";
export { debug } from "./debug"; export { debug } from "./debug";
export { user } from "./user"; export { user } from "./user";
export { create } from "./create"; export { create } from "./create";
export { copyAssets } from "./copy-assets";

View File

@@ -468,13 +468,18 @@ export class ModuleManager {
}); });
} }
async build() { async build(opts?: { fetch?: boolean }) {
this.logger.context("build").log("version", this.version()); this.logger.context("build").log("version", this.version());
this.logger.log("booted with", this._booted_with); this.logger.log("booted with", this._booted_with);
// if no config provided, try fetch from db // if no config provided, try fetch from db
if (this.version() === 0) { if (this.version() === 0 || opts?.fetch === true) {
this.logger.context("no version").log("version is 0"); if (this.version() === 0) {
this.logger.context("no version").log("version is 0");
} else {
this.logger.context("force fetch").log("force fetch");
}
try { try {
const result = await this.fetch(); const result = await this.fetch();

View File

@@ -20,10 +20,11 @@ export class SystemApi extends ModuleApi<any> {
return this.get<{ version: number } & ModuleConfigs>("config"); return this.get<{ version: number } & ModuleConfigs>("config");
} }
readSchema(options?: { config?: boolean; secrets?: boolean }) { readSchema(options?: { config?: boolean; secrets?: boolean; fresh?: boolean }) {
return this.get<ApiSchemaResponse>("schema", { return this.get<ApiSchemaResponse>("schema", {
config: options?.config ? 1 : 0, config: options?.config ? 1 : 0,
secrets: options?.secrets ? 1 : 0, secrets: options?.secrets ? 1 : 0,
fresh: options?.fresh ? 1 : 0,
}); });
} }

View File

@@ -164,13 +164,23 @@ export class AdminController extends Controller {
}; };
if (isProd) { if (isProd) {
// @ts-ignore let manifest: any;
const manifest = await import("bknd/dist/manifest.json", { if (this.options.assets_path.startsWith("http")) {
assert: { type: "json" }, manifest = await fetch(this.options.assets_path + "manifest.json", {
}); headers: {
Accept: "application/json",
},
}).then((res) => res.json());
} else {
// @ts-ignore
manifest = await import("bknd/dist/manifest.json", {
assert: { type: "json" },
}).then((res) => res.default);
}
// @todo: load all marked as entry (incl. css) // @todo: load all marked as entry (incl. css)
assets.js = manifest.default["src/ui/main.tsx"].file; assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest.default["src/ui/main.tsx"].css[0] as any; assets.css = manifest["src/ui/main.tsx"].css[0] as any;
} }
const theme = configs.server.admin.color_scheme ?? "light"; const theme = configs.server.admin.color_scheme ?? "light";
@@ -197,16 +207,8 @@ export class AdminController extends Controller {
)} )}
{isProd ? ( {isProd ? (
<Fragment> <Fragment>
<script <script type="module" src={this.options.assets_path + assets?.js} />
type="module" <link rel="stylesheet" href={this.options.assets_path + assets?.css} />
CrossOrigin
src={this.options.assets_path + assets?.js}
/>
<link
rel="stylesheet"
crossOrigin
href={this.options.assets_path + assets?.css}
/>
</Fragment> </Fragment>
) : ( ) : (
<Fragment> <Fragment>

View File

@@ -1,7 +1,7 @@
/// <reference types="@cloudflare/workers-types" /> /// <reference types="@cloudflare/workers-types" />
import type { App } from "App"; import type { App } from "App";
import { tbValidator as tb } from "core"; import { $console, tbValidator as tb } from "core";
import { import {
StringEnum, StringEnum,
Type, Type,
@@ -229,17 +229,23 @@ export class SystemController extends Controller {
Type.Object({ Type.Object({
config: Type.Optional(booleanLike), config: Type.Optional(booleanLike),
secrets: Type.Optional(booleanLike), secrets: Type.Optional(booleanLike),
fresh: Type.Optional(booleanLike),
}), }),
), ),
async (c) => { async (c) => {
const module = c.req.param("module") as ModuleKey | undefined; const module = c.req.param("module") as ModuleKey | undefined;
const { config, secrets } = c.req.valid("query"); const { config, secrets, fresh } = c.req.valid("query");
config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c); config && this.ctx.guard.throwUnlessGranted(SystemPermissions.configRead, c);
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c); secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c);
const { version, ...schema } = this.app.getSchema(); const { version, ...schema } = this.app.getSchema();
if (fresh) {
// in cases of concurrency, refetching schema/config must be always fresh
await this.app.build({ fetch: true });
}
if (module) { if (module) {
return c.json({ return c.json({
module, module,
@@ -265,14 +271,18 @@ export class SystemController extends Controller {
"query", "query",
Type.Object({ Type.Object({
sync: Type.Optional(booleanLike), sync: Type.Optional(booleanLike),
fetch: Type.Optional(booleanLike),
}), }),
), ),
async (c) => { async (c) => {
const { sync } = c.req.valid("query") as Record<string, boolean>; const options = c.req.valid("query") as Record<string, boolean>;
this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c); this.ctx.guard.throwUnlessGranted(SystemPermissions.build, c);
await this.app.build({ sync }); await this.app.build(options);
return c.json({ success: true, options: { sync } }); return c.json({
success: true,
options,
});
}, },
); );

View File

@@ -47,19 +47,29 @@ export function BkndProvider({
const api = useApi(); const api = useApi();
async function reloadSchema() { async function reloadSchema() {
await fetchSchema(includeSecrets, true); await fetchSchema(includeSecrets, {
force: true,
fresh: true,
});
} }
async function fetchSchema(_includeSecrets: boolean = false, force?: boolean) { async function fetchSchema(
_includeSecrets: boolean = false,
opts?: {
force?: boolean;
fresh?: boolean;
},
) {
const requesting = withSecrets ? Fetching.Secrets : Fetching.Schema; const requesting = withSecrets ? Fetching.Secrets : Fetching.Schema;
if (fetching.current === requesting) return; if (fetching.current === requesting) return;
if (withSecrets && !force) return; if (withSecrets && opts?.force !== true) return;
fetching.current = requesting; fetching.current = requesting;
const res = await api.system.readSchema({ const res = await api.system.readSchema({
config: true, config: true,
secrets: _includeSecrets, secrets: _includeSecrets,
fresh: opts?.fresh,
}); });
if (!res.ok) { if (!res.ok) {

View File

@@ -1,4 +1,4 @@
import { IconSettings } from "@tabler/icons-react"; import { IconRefresh, IconSettings } from "@tabler/icons-react";
import { ucFirst } from "core/utils"; import { ucFirst } from "core/utils";
import { useBknd } from "ui/client/bknd"; import { useBknd } from "ui/client/bknd";
import { Empty } from "ui/components/display/Empty"; import { Empty } from "ui/components/display/Empty";
@@ -12,11 +12,16 @@ import { AuthSettings } from "./routes/auth.settings";
import { DataSettings } from "./routes/data.settings"; import { DataSettings } from "./routes/data.settings";
import { FlowsSettings } from "./routes/flows.settings"; import { FlowsSettings } from "./routes/flows.settings";
import { ServerSettings } from "./routes/server.settings"; import { ServerSettings } from "./routes/server.settings";
import { IconButton } from "ui/components/buttons/IconButton";
function SettingsSidebar() { function SettingsSidebar() {
const { version, schema } = useBknd(); const { version, schema, actions } = useBknd();
useBrowserTitle(["Settings"]); useBrowserTitle(["Settings"]);
async function handleRefresh() {
await actions.reload();
}
const modules = Object.keys(schema).map((key) => { const modules = Object.keys(schema).map((key) => {
return { return {
title: schema[key].title ?? ucFirst(key), title: schema[key].title ?? ucFirst(key),
@@ -26,7 +31,14 @@ function SettingsSidebar() {
return ( return (
<AppShell.Sidebar> <AppShell.Sidebar>
<AppShell.SectionHeader right={<span className="font-mono">v{version}</span>}> <AppShell.SectionHeader
right={
<div className="flex items-center gap-2">
<span className="font-mono leading-none">v{version}</span>
<IconButton Icon={IconRefresh} onClick={handleRefresh} />
</div>
}
>
Settings Settings
</AppShell.SectionHeader> </AppShell.SectionHeader>
<AppShell.Scrollable initialOffset={96}> <AppShell.Scrollable initialOffset={96}>

3824
bun.lock

File diff suppressed because it is too large Load Diff

1378
docs/bun.lock Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

107
docs/integration/aws.mdx Normal file
View File

@@ -0,0 +1,107 @@
---
title: 'AWS Lambda'
description: 'Run bknd inside AWS Lambda'
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
To get started with AWS Lambda and bknd you can either install the package manually and follow the descriptions below, or use the CLI starter:
<Tabs>
<Tab title="CLI Starter">
Create a new Bun CLI starter project by running the following command:
```sh
npx bknd create -i aws
```
</Tab>
<Tab title="Manual">
Create a new AWS Lambda project and then install bknd as a dependency:
<InstallBknd />
</Tab>
</Tabs>
## Serve the API
To serve the API, you can use the `serveLambda` function of the AWS Lambda adapter.
```tsx index.mjs
import { serveLambda } from "bknd/adapter/aws";
export const handler = serveLambda({
connection: {
url: process.env.DB_URL!,
authToken: process.env.DB_AUTH_TOKEN!
}
});
```
Although the runtime would support database as a file, we don't recommend it. You'd need to also bundle the native dependencies which increases the deployment size and cold start time.
## Serve the Admin UI
Lambda functions should be as small as possible. Therefore, the static files for the admin panel should not be served from node_modules like with the Node adapter.
Instead, we recommend to copy the static files and bundle them with the lambda function. To copy the static files, you can use the `copy-assets` command:
```bash
npx bknd copy-assets --out static
```
This will copy the static files to the `static` directory and then serve them from there:
```tsx index.mjs {8-11}
import { serveLambda } from "bknd/adapter/aws";
export const handler = serveLambda({
connection: {
url: process.env.DB_URL!,
authToken: process.env.DB_AUTH_TOKEN!
},
assets: {
mode: "local",
root: "./static"
}
});
```
## Deployment
To deploy a lambda function, you could follow these steps:
1. Create an IAM role with a trust policy that allows lambda to assume the role.
2. Attach the `AWSLambdaBasicExecutionRole` policy to the role.
3. Bundle the lambda function with the static files (e.g. using esbuild)
4. Create a zip file with the bundled lambda function
5. Create a lambda function
6. Create a function URL for the lambda function & make it publicly accessible (optional)
Depending on your use case, you may want to skip step 6 and use the AWS API Gateway to serve the lambda function. Here is an [example deployment script](https://github.com/bknd-io/bknd/blob/main/examples/aws-lambda/deploy.sh) which creates the AWS resources described above, bundles the lambda function and uploads it.
### Using the CLI starter
The CLI starter example includes a basic build script that creates the required AWS resources, copies the static files, bundles the lambda function and uploads it. To deploy the lambda function, you can run:
```bash
npm run deploy
```
To make adjustments to the lambda function created (e.g. architecture, memory, timeout, etc.) you can edit the head section of the `deploy.sh` script.
```sh deploy.sh
# cat deploy.sh | head -12
FUNCTION_NAME="bknd-lambda"
ROLE_NAME="bknd-lambda-execution-role"
RUNTIME="nodejs22.x"
HANDLER="index.handler"
ARCHITECTURE="arm64" # or "x86_64"
MEMORY="1024" # in MB, 128 is the minimum
TIMEOUT="30"
ENTRY_FILE="index.mjs"
ZIP_FILE="lambda.zip"
# ...
```
To clean up AWS resources created by the deployment script, you can run:
```bash
npm run clean
```

View File

@@ -3,7 +3,7 @@ title: 'Introduction'
description: 'Integrate bknd into your runtime/framework of choice' description: 'Integrate bknd into your runtime/framework of choice'
--- ---
import { cloudflare, nextjs, remix, astro, bun, node, docker, vite } from "/snippets/integration-icons.mdx" import { cloudflare, nextjs, remix, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx"
## Start with a Framework ## Start with a Framework
bknd seamlessly integrates with popular frameworks, allowing you to use what you're already familar with. The following guides will help you get started with your framework of choice. bknd seamlessly integrates with popular frameworks, allowing you to use what you're already familar with. The following guides will help you get started with your framework of choice.
@@ -52,6 +52,11 @@ If you prefer to use a runtime instead of a framework, you can choose from the f
icon={<div className="text-primary-light">{cloudflare}</div>} icon={<div className="text-primary-light">{cloudflare}</div>}
href="/integration/cloudflare" href="/integration/cloudflare"
/> />
<Card
title="AWS Lambda"
icon={<div className="text-primary-light">{aws}</div>}
href="/integration/aws"
/>
<Card <Card
title="Vite" title="Vite"
icon={<div className="text-primary-light">{vite}</div>} icon={<div className="text-primary-light">{vite}</div>}
@@ -62,13 +67,15 @@ If you prefer to use a runtime instead of a framework, you can choose from the f
icon={<div className="text-primary-light">{docker}</div>} icon={<div className="text-primary-light">{docker}</div>}
href="/integration/docker" href="/integration/docker"
/> />
<Card <div style={{ gridColumn: "span 2" }}>
horizontal <Card
title="Yours missing?" horizontal
href="https://github.com/bknd-io/bknd/issues/new" title="Yours missing?"
> href="https://github.com/bknd-io/bknd/issues/new"
Create a new issue to request a guide for your runtime. >
</Card> Create a new issue to request a guide for your runtime.
</Card>
</div>
</CardGroup> </CardGroup>
## Overview ## Overview

View File

@@ -2,16 +2,12 @@
title: Introduction title: Introduction
--- ---
import { cloudflare, nextjs, remix, astro, bun, node, docker, vite } from "/snippets/integration-icons.mdx" import { cloudflare, nextjs, remix, astro, bun, node, docker, vite, aws } from "/snippets/integration-icons.mdx"
import { Stackblitz, examples } from "/snippets/stackblitz.mdx" import { Stackblitz, examples } from "/snippets/stackblitz.mdx"
Glad you're here! This is about **bknd**, a feature-rich backend that is so lightweight it could Glad you're here! This is about **bknd**, a feature-rich backend that is so lightweight it could
run on your toaster (probably). run on your toaster (probably).
<Note>
The documentation is currently a work in progress and not complete. Updates will be made regularily.
</Note>
## Preview ## Preview
Here is a preview of **bknd** in StackBlitz: Here is a preview of **bknd** in StackBlitz:
<Stackblitz {...examples.adminRich} /> <Stackblitz {...examples.adminRich} />
@@ -72,6 +68,11 @@ in the future, so stay tuned!
icon={<div className="text-primary-light">{bun}</div>} icon={<div className="text-primary-light">{bun}</div>}
href="/integration/bun" href="/integration/bun"
/> />
<Card
title="AWS Lambda"
icon={<div className="text-primary-light">{aws}</div>}
href="/integration/aws"
/>
<Card <Card
title="Vite" title="Vite"
icon={<div className="text-primary-light">{vite}</div>} icon={<div className="text-primary-light">{vite}</div>}
@@ -82,4 +83,11 @@ in the future, so stay tuned!
icon={<div className="text-primary-light">{docker}</div>} icon={<div className="text-primary-light">{docker}</div>}
href="/integration/docker" href="/integration/docker"
/> />
<Card
horizontal
title="Yours missing?"
href="https://github.com/bknd-io/bknd/issues/new"
>
Create a new issue to request a guide for your runtime or framework.
</Card>
</CardGroup> </CardGroup>

View File

@@ -94,6 +94,7 @@
"integration/bun", "integration/bun",
"integration/cloudflare", "integration/cloudflare",
"integration/deno", "integration/deno",
"integration/aws",
"integration/docker" "integration/docker"
] ]
} }
@@ -110,20 +111,6 @@
"modules/flows" "modules/flows"
] ]
}, },
{
"group": "Deployment",
"pages": [
"deployment/overview",
{
"group": "Providers",
"pages": [
"deployment/providers/cloudflare",
"deployment/providers/vercel",
"deployment/providers/aws"
]
}
]
},
{ {
"group": "User Guide", "group": "User Guide",
"pages": ["guide/introduction", "guide/setup", "guide/admin UI"] "pages": ["guide/introduction", "guide/setup", "guide/admin UI"]

View File

@@ -1,5 +1,5 @@
{ {
"name": "@bknd/docs", "name": "bknd-docs",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "mintlify dev" "dev": "mintlify dev"

View File

@@ -28,4 +28,6 @@ export const vite = <svg xmlns="http://www.w3.org/2000/svg" width="28" height="2
</svg> </svg>
export const docker = <svg xmlns="http://www.w3.org/2000/svg" width={30} height={30} viewBox="0 0 24 24"><path export const docker = <svg xmlns="http://www.w3.org/2000/svg" width={30} height={30} viewBox="0 0 24 24"><path
fill="currentColor" d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43c-.28 0-.56.03-.84.08c-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17l-.18.27c-.24.36-.43.77-.51 1.19c-.2.8-.08 1.56.33 2.21c-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63c0 1.15.18 2.3.58 3.38c.45 1.19 1.13 2.07 2 2.61c.98.6 2.59.94 4.42.94c.79 0 1.61-.07 2.42-.22c1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85c.26-.24.45-.53.59-.87l.08-.24zm-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"></path></svg> fill="currentColor" d="M21.81 10.25c-.06-.04-.56-.43-1.64-.43c-.28 0-.56.03-.84.08c-.21-1.4-1.38-2.11-1.43-2.14l-.29-.17l-.18.27c-.24.36-.43.77-.51 1.19c-.2.8-.08 1.56.33 2.21c-.49.28-1.29.35-1.46.35H2.62c-.34 0-.62.28-.62.63c0 1.15.18 2.3.58 3.38c.45 1.19 1.13 2.07 2 2.61c.98.6 2.59.94 4.42.94c.79 0 1.61-.07 2.42-.22c1.12-.2 2.2-.59 3.19-1.16A8.3 8.3 0 0 0 16.78 16c1.05-1.17 1.67-2.5 2.12-3.65h.19c1.14 0 1.85-.46 2.24-.85c.26-.24.45-.53.59-.87l.08-.24zm-17.96.99h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H3.85c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.43 0h1.76c.08 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16H6.28c-.09 0-.16.07-.16.16v1.58c.01.09.07.16.16.16m2.47 0h1.75c.1 0 .17-.07.17-.16V9.5c0-.08-.06-.16-.17-.16H8.75c-.08 0-.15.07-.15.16v1.58c0 .09.06.16.15.16m2.44 0h1.77c.08 0 .15-.07.15-.16V9.5c0-.08-.06-.16-.15-.16h-1.77c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16M6.28 9h1.76c.08 0 .16-.09.16-.18V7.25c0-.09-.07-.16-.16-.16H6.28c-.09 0-.16.06-.16.16v1.57c.01.09.07.18.16.18m2.47 0h1.75c.1 0 .17-.09.17-.18V7.25c0-.09-.06-.16-.17-.16H8.75c-.08 0-.15.06-.15.16v1.57c0 .09.06.18.15.18m2.44 0h1.77c.08 0 .15-.09.15-.18V7.25c0-.09-.07-.16-.15-.16h-1.77c-.08 0-.15.06-.15.16v1.57c0 .09.07.18.15.18m0-2.28h1.77c.08 0 .15-.07.15-.16V5c0-.1-.07-.17-.15-.17h-1.77c-.08 0-.15.06-.15.17v1.56c0 .08.07.16.15.16m2.46 4.52h1.76c.09 0 .16-.07.16-.16V9.5c0-.08-.07-.16-.16-.16h-1.76c-.08 0-.15.07-.15.16v1.58c0 .09.07.16.15.16"></path></svg>
export const aws = <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="currentColor" d="M4.986 0a.545.545 0 0 0-.534.548l-.006 4.908c0 .145.06.283.159.39a.53.53 0 0 0 .38.155h3.429l8.197 17.68a.54.54 0 0 0 .488.319h5.811a.547.547 0 0 0 .543-.548v-4.908a.543.543 0 0 0-.543-.548h-2.013L12.739.316A.55.55 0 0 0 12.245 0H4.991Zm.54 1.09h6.367l8.16 17.681a.54.54 0 0 0 .489.318h1.817v3.817h-4.922L9.24 5.226a.54.54 0 0 0-.488-.318h-3.23Zm2.013 8.237a.54.54 0 0 0-.486.31L.6 23.213a.55.55 0 0 0 .032.528a.53.53 0 0 0 .454.25h6.169a.55.55 0 0 0 .497-.31l3.38-7.165a.54.54 0 0 0-.003-.469l-3.093-6.41a.55.55 0 0 0-.494-.31Zm.006 1.804l2.488 5.152l-3.122 6.62H1.947Z" stroke-width="0.5" stroke="currentColor"/></svg>

53
examples/aws-lambda/clean.sh Executable file
View File

@@ -0,0 +1,53 @@
#!/bin/bash
# Set variables
FUNCTION_NAME="bknd-lambda"
ROLE_NAME="bknd-lambda-execution-role"
echo "Starting cleanup of AWS resources..."
# Delete Function URL if it exists
echo "Checking if Function URL exists for '$FUNCTION_NAME'..."
FUNCTION_URL=$(aws lambda get-function-url-config --function-name $FUNCTION_NAME --query "FunctionUrl" --output text 2>/dev/null)
if [ -n "$FUNCTION_URL" ]; then
echo "Deleting Function URL for '$FUNCTION_NAME'..."
aws lambda delete-function-url-config --function-name $FUNCTION_NAME
echo "Function URL deleted."
else
echo "No Function URL found for '$FUNCTION_NAME'."
fi
# Delete Lambda function if it exists
echo "Checking if Lambda function '$FUNCTION_NAME' exists..."
LAMBDA_EXISTS=$(aws lambda get-function --function-name $FUNCTION_NAME --query "Configuration.FunctionArn" --output text 2>/dev/null)
if [ -n "$LAMBDA_EXISTS" ]; then
echo "Deleting Lambda function '$FUNCTION_NAME'..."
aws lambda delete-function --function-name $FUNCTION_NAME
echo "Lambda function deleted."
else
echo "Lambda function '$FUNCTION_NAME' does not exist."
fi
# Delete IAM role and attached policies if role exists
echo "Checking if IAM role '$ROLE_NAME' exists..."
ROLE_EXISTS=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text 2>/dev/null)
if [ -n "$ROLE_EXISTS" ]; then
echo "Detaching policies from IAM role '$ROLE_NAME'..."
ATTACHED_POLICIES=$(aws iam list-attached-role-policies --role-name $ROLE_NAME --query "AttachedPolicies[].PolicyArn" --output text)
for POLICY_ARN in $ATTACHED_POLICIES; do
aws iam detach-role-policy --role-name $ROLE_NAME --policy-arn $POLICY_ARN
echo "Detached policy: $POLICY_ARN"
done
echo "Deleting IAM role '$ROLE_NAME'..."
aws iam delete-role --role-name $ROLE_NAME
echo "IAM role deleted."
else
echo "IAM role '$ROLE_NAME' does not exist."
fi
echo "AWS resource cleanup completed successfully!"

131
examples/aws-lambda/deploy.sh Executable file
View File

@@ -0,0 +1,131 @@
#!/bin/bash
# Set variables
FUNCTION_NAME="bknd-lambda"
ROLE_NAME="bknd-lambda-execution-role"
RUNTIME="nodejs22.x"
HANDLER="index.handler"
ARCHITECTURE="arm64" # or "x86_64"
MEMORY="1024" # in MB, 128 is the minimum
TIMEOUT="30"
ENTRY_FILE="index.mjs"
ZIP_FILE="lambda.zip"
# Build step
echo "Building Lambda package..."
rm -rf dist && mkdir dist
# copy assets
node_modules/.bin/bknd copy-assets --out=dist/static
# Run esbuild and check for errors
# important to use --platform=browser for libsql dependency (otherwise we need to push node_modules)
if ! npx esbuild $ENTRY_FILE --bundle --format=cjs --platform=browser --external:fs --minify --outfile=dist/index.js; then
echo "Error: esbuild failed to build the package"
exit 1
fi
# zip
( cd dist && zip -r $ZIP_FILE . )
# Read .env file and export variables
if [ -f .env ]; then
export $(grep -v '^#' .env | xargs)
else
echo ".env file not found!"
exit 1
fi
# Prepare environment variables string for Lambda
ENV_VARS=$(awk -F= '{printf "%s=\"%s\",", $1, $2}' .env | sed 's/,$//')
# Create a trust policy file for the Lambda execution role
cat > trust-policy.json << EOL
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOL
# Create IAM role if it doesn't exist
ROLE_ARN=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text 2>/dev/null)
if [ -z "$ROLE_ARN" ]; then
echo "Creating IAM role..."
aws iam create-role --role-name $ROLE_NAME --assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy --role-name $ROLE_NAME --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# Wait for IAM role to propagate
echo "Waiting for IAM role to propagate..."
sleep 10
# Get the role ARN after creation
ROLE_ARN=$(aws iam get-role --role-name $ROLE_NAME --query "Role.Arn" --output text)
else
echo "Using existing IAM role: $ROLE_ARN"
fi
# Create or update Lambda function
echo "Creating or updating Lambda function..."
LAMBDA_ARN=$(aws lambda get-function --function-name $FUNCTION_NAME --query "Configuration.FunctionArn" --output text 2>/dev/null)
if [ -z "$LAMBDA_ARN" ]; then
echo "Creating new Lambda function..."
aws lambda create-function \
--function-name $FUNCTION_NAME \
--zip-file fileb://dist/$ZIP_FILE \
--handler $HANDLER \
--runtime $RUNTIME \
--role $ROLE_ARN \
--architectures $ARCHITECTURE \
--memory-size $MEMORY \
--timeout $TIMEOUT \
--environment Variables="{$ENV_VARS}"
else
echo "Updating existing Lambda function..."
aws lambda update-function-code --function-name $FUNCTION_NAME --zip-file fileb://dist/$ZIP_FILE
echo "Waiting for Lambda function to become active..."
aws lambda wait function-updated --function-name $FUNCTION_NAME
# Update function configuration, including env variables
aws lambda update-function-configuration \
--function-name $FUNCTION_NAME \
--memory-size $MEMORY \
--timeout $TIMEOUT \
--environment Variables="{$ENV_VARS}"
fi
# Check if Function URL exists, if not create it
echo "Checking if Function URL exists..."
FUNCTION_URL=$(aws lambda get-function-url-config --function-name $FUNCTION_NAME --query "FunctionUrl" --output text 2>/dev/null)
if [ -z "$FUNCTION_URL" ]; then
echo "Creating Function URL..."
FUNCTION_URL=$(aws lambda create-function-url-config \
--function-name $FUNCTION_NAME \
--auth-type NONE \
--query "FunctionUrl" --output text)
# Make the Function URL publicly accessible (log output)
aws lambda add-permission \
--function-name $FUNCTION_NAME \
--action lambda:InvokeFunctionUrl \
--principal "*" \
--statement-id public-access \
--function-url-auth-type NONE
echo "Created Lambda Function URL: $FUNCTION_URL"
else
echo "Lambda Function URL: $FUNCTION_URL"
fi
echo "Deployment completed successfully!"

View File

@@ -0,0 +1,14 @@
import { serveLambda } from "bknd/adapter/aws";
export const handler = serveLambda({
// to get local assets, run `npx bknd copy-assets`
// this is automatically done in `deploy.sh`
assets: {
mode: "local",
root: "./static",
},
connection: {
url: process.env.DB_URL,
authToken: process.env.DB_TOKEN,
},
});

View File

@@ -0,0 +1,21 @@
{
"name": "aws-lambda",
"version": "1.0.0",
"main": "index.mjs",
"scripts": {
"test": "esbuild index.mjs --bundle --format=cjs --platform=node --external:fs --outfile=dist/index.js && node test.js",
"deploy": "./deploy.sh",
"clean": "./clean.sh"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"bknd": "file:../../app/bknd-0.9.0-rc.1-11.tgz"
},
"devDependencies": {
"esbuild": "^0.25.0",
"dotenv": "^16.4.7"
}
}

View File

@@ -0,0 +1,31 @@
require("dotenv").config();
const handler = require("./dist/index.js").handler;
const event = {
httpMethod: "GET",
path: "/",
//path: "/api/system/config",
//path: "/assets/main-B6sEDlfs.js",
headers: {
//"Content-Type": "application/json",
"User-Agent": "curl/7.64.1",
Accept: "*/*",
},
};
const context = {
awsRequestId: "mocked-request-id",
functionName: "myMinimalLambda",
functionVersion: "$LATEST",
memoryLimitInMB: "128",
getRemainingTimeInMillis: () => 5000,
};
// Execute the handler
handler(event, context)
.then((response) => {
console.log(response.statusCode, response.body);
})
.catch((error) => {
console.error("Error:", error);
});

View File

@@ -0,0 +1,12 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}

17
examples/bun/static.ts Normal file
View File

@@ -0,0 +1,17 @@
// @ts-ignore somehow causes types:build issues on app
import { type BunBkndConfig, serve } from "bknd/adapter/bun";
// Actually, all it takes is the following line:
// serve();
// this is optional, if omitted, it uses an in-memory database
const config: BunBkndConfig = {
adminOptions: {
assets_path: "https://cdn.bknd.io/0.9.0-rc.1/",
},
connection: {
url: "file:data.db",
},
};
serve(config);

View File

@@ -44,6 +44,6 @@
}, },
"workspaces": [ "workspaces": [
"app", "app",
"docs" "packages/*"
] ]
} }