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("bun"));
await tsup.build(baseConfig("astro"));
await tsup.build(baseConfig("aws"));
await tsup.build(
baseConfig("cloudflare", {
external: [/^kysely/],

View File

@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"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.",
"homepage": "https://bknd.io",
"repository": {
@@ -191,6 +191,11 @@
"import": "./dist/adapter/astro/index.js",
"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/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json"

View File

@@ -58,6 +58,8 @@ export class App {
adminController?: AdminController;
private trigger_first_boot = false;
private plugins: AppPlugin[];
private _id: string = crypto.randomUUID();
private _building: boolean = false;
constructor(
private connection: Connection,
@@ -90,6 +92,11 @@ export class App {
server.use(async (c, next) => {
c.set("app", this);
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;
}
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;
await this.modules.build();
await this.modules.build({ fetch: options?.fetch });
const { guard, server } = this.modules.ctx();
@@ -127,6 +143,8 @@ export class App {
app: this,
});
}
this._building = false;
}
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,
onBuilt,
buildConfig,
adminOptions,
...serveOptions
}: BunBkndConfig = {}) {
Bun.serve({
@@ -46,6 +47,7 @@ export function serve({
options,
onBuilt,
buildConfig,
adminOptions,
distPath,
});
return app.fetch(request);

View File

@@ -14,6 +14,8 @@ export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
distPath?: string;
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
};
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>(
{
serveStatic,
adminOptions,
...config
}: RuntimeBkndConfig & {
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
adminOptions?: AdminControllerOptions | false;
},
{ serveStatic, adminOptions, ...config }: RuntimeBkndConfig,
env?: Env,
): Promise<App> {
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",
bun: "Bun",
cloudflare: "Cloudflare",
aws: "AWS Lambda",
},
framework: {
nextjs: "Next.js",

View File

@@ -4,3 +4,4 @@ export { run } from "./run";
export { debug } from "./debug";
export { user } from "./user";
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.log("booted with", this._booted_with);
// if no config provided, try fetch from db
if (this.version() === 0) {
this.logger.context("no version").log("version is 0");
if (this.version() === 0 || opts?.fetch === true) {
if (this.version() === 0) {
this.logger.context("no version").log("version is 0");
} else {
this.logger.context("force fetch").log("force fetch");
}
try {
const result = await this.fetch();

View File

@@ -20,10 +20,11 @@ export class SystemApi extends ModuleApi<any> {
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", {
config: options?.config ? 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) {
// @ts-ignore
const manifest = await import("bknd/dist/manifest.json", {
assert: { type: "json" },
});
let manifest: any;
if (this.options.assets_path.startsWith("http")) {
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)
assets.js = manifest.default["src/ui/main.tsx"].file;
assets.css = manifest.default["src/ui/main.tsx"].css[0] as any;
assets.js = manifest["src/ui/main.tsx"].file;
assets.css = manifest["src/ui/main.tsx"].css[0] as any;
}
const theme = configs.server.admin.color_scheme ?? "light";
@@ -197,16 +207,8 @@ export class AdminController extends Controller {
)}
{isProd ? (
<Fragment>
<script
type="module"
CrossOrigin
src={this.options.assets_path + assets?.js}
/>
<link
rel="stylesheet"
crossOrigin
href={this.options.assets_path + assets?.css}
/>
<script type="module" src={this.options.assets_path + assets?.js} />
<link rel="stylesheet" href={this.options.assets_path + assets?.css} />
</Fragment>
) : (
<Fragment>

View File

@@ -1,7 +1,7 @@
/// <reference types="@cloudflare/workers-types" />
import type { App } from "App";
import { tbValidator as tb } from "core";
import { $console, tbValidator as tb } from "core";
import {
StringEnum,
Type,
@@ -229,17 +229,23 @@ export class SystemController extends Controller {
Type.Object({
config: Type.Optional(booleanLike),
secrets: Type.Optional(booleanLike),
fresh: Type.Optional(booleanLike),
}),
),
async (c) => {
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);
secrets && this.ctx.guard.throwUnlessGranted(SystemPermissions.configReadSecrets, c);
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) {
return c.json({
module,
@@ -265,14 +271,18 @@ export class SystemController extends Controller {
"query",
Type.Object({
sync: Type.Optional(booleanLike),
fetch: Type.Optional(booleanLike),
}),
),
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);
await this.app.build({ sync });
return c.json({ success: true, options: { sync } });
await this.app.build(options);
return c.json({
success: true,
options,
});
},
);

View File

@@ -47,19 +47,29 @@ export function BkndProvider({
const api = useApi();
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;
if (fetching.current === requesting) return;
if (withSecrets && !force) return;
if (withSecrets && opts?.force !== true) return;
fetching.current = requesting;
const res = await api.system.readSchema({
config: true,
secrets: _includeSecrets,
fresh: opts?.fresh,
});
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 { useBknd } from "ui/client/bknd";
import { Empty } from "ui/components/display/Empty";
@@ -12,11 +12,16 @@ import { AuthSettings } from "./routes/auth.settings";
import { DataSettings } from "./routes/data.settings";
import { FlowsSettings } from "./routes/flows.settings";
import { ServerSettings } from "./routes/server.settings";
import { IconButton } from "ui/components/buttons/IconButton";
function SettingsSidebar() {
const { version, schema } = useBknd();
const { version, schema, actions } = useBknd();
useBrowserTitle(["Settings"]);
async function handleRefresh() {
await actions.reload();
}
const modules = Object.keys(schema).map((key) => {
return {
title: schema[key].title ?? ucFirst(key),
@@ -26,7 +31,14 @@ function SettingsSidebar() {
return (
<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
</AppShell.SectionHeader>
<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'
---
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
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>}
href="/integration/cloudflare"
/>
<Card
title="AWS Lambda"
icon={<div className="text-primary-light">{aws}</div>}
href="/integration/aws"
/>
<Card
title="Vite"
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>}
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.
</Card>
<div style={{ gridColumn: "span 2" }}>
<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.
</Card>
</div>
</CardGroup>
## Overview

View File

@@ -2,16 +2,12 @@
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"
Glad you're here! This is about **bknd**, a feature-rich backend that is so lightweight it could
run on your toaster (probably).
<Note>
The documentation is currently a work in progress and not complete. Updates will be made regularily.
</Note>
## Preview
Here is a preview of **bknd** in StackBlitz:
<Stackblitz {...examples.adminRich} />
@@ -72,6 +68,11 @@ in the future, so stay tuned!
icon={<div className="text-primary-light">{bun}</div>}
href="/integration/bun"
/>
<Card
title="AWS Lambda"
icon={<div className="text-primary-light">{aws}</div>}
href="/integration/aws"
/>
<Card
title="Vite"
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>}
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>

View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "@bknd/docs",
"name": "bknd-docs",
"private": true,
"scripts": {
"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>
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": [
"app",
"docs"
"packages/*"
]
}