Release 0.16 (#196)

* initial refactor

* fixes

* test secrets extraction

* updated lock

* fix secret schema

* updated schemas, fixed tests, skipping flow tests for now

* added validator for rjsf, hook form via standard schema

* removed @sinclair/typebox

* remove unneeded vite dep

* fix jsonv literal on Field.tsx

* fix schema import path

* fix schema modals

* fix schema modals

* fix json field form, replaced auth form

* initial waku

* finalize waku example

* fix jsonv-ts version

* fix schema updates with falsy values

* fix media api to respect options' init, improve types

* checking media controller test

* checking media controller test

* checking media controller test

* clean up mediacontroller test

* added cookie option `partitioned`, as well as cors `origin` to be array, option to enable `credentials` (#214)

* added cookie option `partitioned`, as well as cors `origin` to be array, option to enable `credentials`

* fix server test

* fix data api (updated jsonv-ts)

* enhance cloudflare image optimization plugin with new options and explain endpoint (#215)

* feat: add ability to serve static by using dynamic imports (#197)

* feat: add ability to serve static by using dynamic imports

* serveStaticViaImport: make manifest optional

* serveStaticViaImport: add error log

* refactor/imports (#217)

* refactored core and core/utils imports

* refactored core and core/utils imports

* refactored media imports

* refactored auth imports

* refactored data imports

* updated package json exports, fixed mm config

* fix tests

* feat/deno (#219)

* update bun version

* fix module manager's em reference

* add basic deno example

* finalize

* docs: fumadocs migration (#185)

* feat(docs): initialize documentation structure with Fumadocs

* feat(docs): remove home route and move /docs route to /route

* feat(docs): add redirect to /start page

* feat(docs): migrate Getting Started chapters

* feat(docs): migrate Usage and Extending chapters

* feat(callout): add CalloutCaution, CalloutDanger, CalloutInfo, and CalloutPositive

* feat(layout): add Discord and GitHub links to documentation layout

* feat(docs): add integration chapters draft

* feat(docs): add modules chapters draft

* refactor(mdx-components): remove unused Icon import

* refactor(StackBlitz): enhance type safety by using unknown instead of any

* refactor(layout): update navigation mode to 'top' in layout configuration

* feat(docs): add @iconify/react package

* docs(mdx-components): add Icon component to MDX components list

* feat(docs): update Next.js integration guide

* feat(docs): update React Router integration guide

* feat(docs): update Astro integration guide

* feat(docs): update Vite integration guide

* fix(docs): update package manager initialization commands

* feat(docs): migrate Modules chapters

* chore(docs): update package.json with new devDependencies

* feat(docs): migrate Integration Runtimes chapters

* feat(docs): update Database usage chapter

* feat(docs): restructure documentation paths

* chore(docs): clean up unused imports and files in documentation

* style(layout): revert navigation mode to previous state

* fix(docs): routing for documentation structure

* feat(openapi): add API documentation generation from OpenAPI schema

* feat(docs): add icons to documentation pages

* chore(dependencies): remove unused content-collections packages

* fix(types): fix type error for attachFile in source.ts

* feat(redirects): update root redirect destination to '/start'

* feat(search): add static search functionality

* chore(dependencies): update fumadocs-core and fumadocs-ui to latest versions

* feat(search): add Powered by Orama link

* feat(generate-openapi): add error handling for missing OpenAPI schema

* feat(scripts): add OpenAPI generation to build process

* feat(config): enable dynamic redirects and rewrites in development mode

* feat(layout): add GitHub token support for improved API rate limits

* feat(redirects): add 301 redirects for cloudflare pages

* feat(docs): add Vercel redirects configuration

* feat(config): enable standalone output for development environment

* chore(layout): adjust layout settings

* refactor(package): clean up ajv dependency versions

* feat(docs): add twoslash support

* refactor(layout): update DocsLayout import and navigation configuration

* chore(layout): clean up layout.tsx by commenting out GithubInfo

* fix(Search): add locale to search initialization

* chore(package): update fumadocs and orama to latest versions

* docs: add menu items descriptions

* feat(layout): add GitHub URL to the layout component

* feat(docs): add AutoTypeTable component to MDX components

* feat(app): implement AutoTypeTable rendering for AppEvents type

* docs(layout): switch callouts back to default components

* fix(config): use __filename and __dirname for module paths

* docs: add note about node.js 22 requirement

* feat(styles): add custom color variables for light and dark themes

* docs: add S3 setup instructions for media module

* docs: fix typos and indentation in media module docs

* docs: add local media adapter example for Node.js

* docs(media): add S3/R2 URL format examples and fix typo

* docs: add cross-links to initial config and seeding sections

* indent numbered lists content, clarified media serve locations

* fix mediacontroller tests

* feat(layout): add AnimatedGridPattern component for dynamic background

* style(layout): configure fancy ToC style ('clerk')

* fix(AnimatedGridPattern): correct strokeDasharray type

* docs: actualize docs

* feat: add favicon

* style(cloudflare): format code examples

* feat(layout): add Github and Discord footer icons

* feat(footer): add SVG social media icons for GitHub and Discord

* docs: adjusted auto type table, added llm functions

* added static deployment to cloudflare workers

* docs: change cf redirects to proxy *.mdx instead of redirecting

---------

Co-authored-by: dswbx <dennis.senn@gmx.ch>
Co-authored-by: cameronapak <cameronandrewpak@gmail.com>

* build: improve build script

* add missing exports, fix EntityTypescript imports

* media: Dropzone: add programmatic upload, additional events, loading state

* schema object: disable extended defaults to allow empty config values

* Feat/new docs deploy (#224)

* test

* try fixing pm

* try fixing pm

* fix docs on imports, export events correctly

---------

Co-authored-by: Tim Seriakov <59409712+timseriakov@users.noreply.github.com>
Co-authored-by: cameronapak <cameronandrewpak@gmail.com>
This commit is contained in:
dswbx
2025-08-01 15:55:59 +02:00
committed by GitHub
parent daaaae82b6
commit a298b65abf
430 changed files with 15041 additions and 12375 deletions

3
docs/.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

29
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# deps
/node_modules
# generated content
.contentlayer
.content-collections
.source
.twoslash-cache
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts

45
docs/README.md Normal file
View File

@@ -0,0 +1,45 @@
# bknd-docs
This is a Next.js application generated with
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
Run development server:
```bash
npm run dev
# or
pnpm dev
# or
yarn dev
```
Open http://localhost:3000 with your browser to see the result.
## Explore
In the project, you can see:
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
- `app/layout.config.tsx`: Shared options for layouts, optional but preferred to keep.
| Route | Description |
| ------------------------- | ------------------------------------------------------ |
| `app/(home)` | The route group for your landing page and other pages. |
| `app/docs` | The documentation layout and pages. |
| `app/api/search/route.ts` | The Route Handler for search. |
### Fumadocs MDX
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
## Learn More
To learn more about Next.js and Fumadocs, take a look at the following
resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
- [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

View File

@@ -1,161 +0,0 @@
<svg width="700" height="320" viewBox="0 0 700 320" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2862_30)">
<rect width="700" height="320" rx="16" fill="url(#paint0_linear_2862_30)"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="white"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="url(#paint1_radial_2862_30)"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="black" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="url(#paint2_linear_2862_30)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M311.72 247.034C283.108 246.887 258.409 231.208 246.538 201.531C234.656 171.825 238.271 134.702 253.583 101.377C282.195 101.524 306.894 117.203 318.765 146.88C330.647 176.586 327.031 213.709 311.72 247.034Z" stroke="url(#paint3_linear_2862_30)" stroke-opacity="0.05" stroke-width="0.530516"/>
<path d="M305.839 247.174C343.92 237.419 377.154 210.619 393.585 171.64C410.017 132.661 405.98 90.1988 386.347 56.1934C348.266 65.9477 315.032 92.7486 298.601 131.728C282.169 170.706 286.206 213.168 305.839 247.174Z" fill="white"/>
<path d="M305.839 247.174C343.92 237.419 377.154 210.619 393.585 171.64C410.017 132.661 405.98 90.1988 386.347 56.1934C348.266 65.9477 315.032 92.7486 298.601 131.728C282.169 170.706 286.206 213.168 305.839 247.174Z" fill="url(#paint4_radial_2862_30)"/>
<path d="M393.341 171.537C376.971 210.369 343.89 237.091 305.969 246.867C286.462 212.959 282.476 170.663 298.845 131.831C315.215 92.9978 348.295 66.2765 386.217 56.5004C405.724 90.4077 409.71 132.704 393.341 171.537Z" stroke="url(#paint5_linear_2862_30)" stroke-opacity="0.05" stroke-width="0.530516"/>
<path d="M305.686 246.995C329.749 266.114 361.965 272.832 393.67 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.045 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="white"/>
<path d="M305.686 246.995C329.749 266.114 361.965 272.832 393.67 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.045 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="url(#paint6_radial_2862_30)"/>
<path d="M305.686 246.995C329.749 266.114 361.965 272.832 393.67 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.045 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="black" fill-opacity="0.2" style="mix-blend-mode:hard-light"/>
<path d="M305.686 246.995C329.749 266.114 361.965 272.832 393.67 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.045 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="url(#paint7_linear_2862_30)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M393.586 261.878C362.034 272.529 329.98 265.88 306.002 246.907C317.534 215.919 341.57 190.327 373.13 179.673C404.681 169.023 436.735 175.671 460.714 194.644C449.181 225.632 425.145 251.224 393.586 261.878Z" stroke="url(#paint8_linear_2862_30)" stroke-opacity="0.05" stroke-width="0.530516"/>
<g opacity="0.8" filter="url(#filter0_f_2862_30)">
<circle cx="660" cy="-60" r="160" fill="#18E244" fill-opacity="0.4"/>
</g>
<g opacity="0.8" filter="url(#filter1_f_2862_30)">
<circle cx="20" cy="213" r="160" fill="#18CAE2" fill-opacity="0.33"/>
</g>
<g opacity="0.8" filter="url(#filter2_f_2862_30)">
<circle cx="660" cy="480" r="160" fill="#18E2B2" fill-opacity="0.52"/>
</g>
<g opacity="0.8" filter="url(#filter3_f_2862_30)">
<circle cx="20" cy="413" r="160" fill="#4018E2" fill-opacity="0.22"/>
</g>
<path opacity="0.2" d="M0 50H700" stroke="url(#paint9_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.1" d="M0 82H700" stroke="url(#paint10_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.2" d="M239 0L239 320" stroke="url(#paint11_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.1" d="M271 0L271 320" stroke="url(#paint12_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.2" d="M461 0L461 320" stroke="url(#paint13_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.1" d="M429 0L429 320" stroke="url(#paint14_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.2" d="M0 271H700" stroke="url(#paint15_radial_2862_30)" stroke-dasharray="4 4"/>
<path opacity="0.1" d="M0 239H700" stroke="url(#paint16_radial_2862_30)" stroke-dasharray="4 4"/>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M0 160H700" stroke="url(#paint17_linear_2862_30)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.2">
<path d="M511 -1L189 321" stroke="url(#paint18_linear_2862_30)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.2">
<path d="M511 321L189 -1" stroke="url(#paint19_linear_2862_30)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<circle cx="350" cy="160" r="111" stroke="white"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<circle cx="350" cy="160" r="79" stroke="white"/>
</g>
</g>
<defs>
<filter id="filter0_f_2862_30" x="260" y="-460" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_30"/>
</filter>
<filter id="filter1_f_2862_30" x="-380" y="-187" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_30"/>
</filter>
<filter id="filter2_f_2862_30" x="260" y="80" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_30"/>
</filter>
<filter id="filter3_f_2862_30" x="-380" y="13" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_30"/>
</filter>
<linearGradient id="paint0_linear_2862_30" x1="1.04308e-05" y1="320" x2="710.784" y2="26.0793" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299" stop-opacity="0.09"/>
<stop offset="0.729167" stop-color="#0D9373" stop-opacity="0.08"/>
</linearGradient>
<radialGradient id="paint1_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(208.697 189.703) rotate(-10.029) scale(169.097 167.466)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint2_linear_2862_30" x1="306.587" y1="93.5598" x2="252.341" y2="224.228" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint3_linear_2862_30" x1="311.84" y1="123.717" x2="253.579" y2="224.761" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint4_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(313.407 243.64) rotate(-75.7542) scale(203.632 223.902)">
<stop stop-color="#00BBBB"/>
<stop offset="0.712616" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint5_linear_2862_30" x1="308.586" y1="102.284" x2="383.487" y2="201.169" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint6_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(311.446 249.925) rotate(-20.3524) scale(174.776 163.096)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint7_linear_2862_30" x1="395.842" y1="169.781" x2="332.121" y2="263.82" gradientUnits="userSpaceOnUse">
<stop stop-color="#00B1BC"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint8_linear_2862_30" x1="395.842" y1="169.781" x2="370.99" y2="271.799" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint9_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(350 50) scale(398.125 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint10_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(350 82) scale(398.125 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint11_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(239 160) rotate(90) scale(182 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint12_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(271 160) rotate(90) scale(182 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint13_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(461 160) rotate(90) scale(182 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint14_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(429 160) rotate(90) scale(182 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint15_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(350 271) scale(398.125 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<radialGradient id="paint16_radial_2862_30" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(350 239) scale(398.125 182)">
<stop offset="0.348958" stop-color="#84FFD3"/>
<stop offset="0.880208" stop-color="#18E299" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint17_linear_2862_30" x1="0" y1="160" x2="700" y2="160" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.1"/>
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint18_linear_2862_30" x1="511" y1="-1" x2="189" y2="321" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.1"/>
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint19_linear_2862_30" x1="511" y1="321" x2="189" y2="-0.999997" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.1"/>
<stop offset="0.5" stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
</linearGradient>
<clipPath id="clip0_2862_30">
<rect width="700" height="320" rx="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,155 +0,0 @@
<svg width="700" height="320" viewBox="0 0 700 320" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2862_278)">
<rect width="700" height="320" rx="16" fill="url(#paint0_linear_2862_278)"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="white"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="url(#paint1_radial_2862_278)"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="black" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M311.889 247.3C283.097 247.215 258.226 231.466 246.292 201.629C234.357 171.793 238.02 134.523 253.414 101.112C282.206 101.197 307.077 116.945 319.011 146.782C330.946 176.619 327.283 213.888 311.889 247.3Z" fill="url(#paint2_linear_2862_278)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M311.72 247.034C283.108 246.887 258.409 231.208 246.538 201.531C234.656 171.825 238.271 134.702 253.583 101.377C282.195 101.524 306.894 117.203 318.765 146.88C330.647 176.586 327.031 213.709 311.72 247.034Z" stroke="url(#paint3_linear_2862_278)" stroke-opacity="0.05" stroke-width="0.530516"/>
<path d="M305.839 247.174C343.92 237.419 377.154 210.619 393.585 171.64C410.017 132.661 405.98 90.1988 386.347 56.1934C348.266 65.9477 315.032 92.7486 298.601 131.728C282.169 170.706 286.206 213.168 305.839 247.174Z" fill="white"/>
<path d="M305.839 247.174C343.92 237.419 377.154 210.619 393.585 171.64C410.017 132.661 405.98 90.1988 386.347 56.1934C348.266 65.9477 315.032 92.7486 298.601 131.728C282.169 170.706 286.206 213.168 305.839 247.174Z" fill="url(#paint4_radial_2862_278)"/>
<path d="M393.341 171.537C376.971 210.369 343.89 237.091 305.969 246.867C286.462 212.959 282.476 170.663 298.845 131.831C315.215 92.9978 348.295 66.2765 386.217 56.5004C405.724 90.4077 409.71 132.704 393.341 171.537Z" stroke="url(#paint5_linear_2862_278)" stroke-opacity="0.05" stroke-width="0.530516"/>
<path d="M305.686 246.995C329.75 266.114 361.965 272.832 393.671 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.046 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="white"/>
<path d="M305.686 246.995C329.75 266.114 361.965 272.832 393.671 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.046 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="url(#paint6_radial_2862_278)"/>
<path d="M305.686 246.995C329.75 266.114 361.965 272.832 393.671 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.046 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="black" fill-opacity="0.2" style="mix-blend-mode:hard-light"/>
<path d="M305.686 246.995C329.75 266.114 361.965 272.832 393.671 262.129C425.376 251.426 449.499 225.691 461.03 194.556C436.967 175.437 404.751 168.719 373.046 179.422C341.34 190.125 317.217 215.86 305.686 246.995Z" fill="url(#paint7_linear_2862_278)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M393.586 261.878C362.035 272.529 329.981 265.88 306.002 246.907C317.535 215.919 341.571 190.327 373.13 179.673C404.682 169.023 436.736 175.671 460.715 194.644C449.182 225.632 425.146 251.224 393.586 261.878Z" stroke="url(#paint8_linear_2862_278)" stroke-opacity="0.05" stroke-width="0.530516"/>
<g opacity="0.8" filter="url(#filter0_f_2862_278)">
<circle cx="660" cy="-60" r="160" fill="#18E299" fill-opacity="0.4"/>
</g>
<g opacity="0.8" filter="url(#filter1_f_2862_278)">
<circle cx="20" cy="213" r="160" fill="#18E299" fill-opacity="0.33"/>
</g>
<g opacity="0.8" filter="url(#filter2_f_2862_278)">
<circle cx="660" cy="480" r="160" fill="#18E299" fill-opacity="0.52"/>
</g>
<g opacity="0.8" filter="url(#filter3_f_2862_278)">
<circle cx="20" cy="413" r="160" fill="#18E299" fill-opacity="0.22"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M0 50H700" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M0 82H700" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M239 0L239 320" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M271 0L271 320" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M461 0L461 320" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M350 0L350 320" stroke="url(#paint9_linear_2862_278)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M429 0L429 320" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M0 271H700" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M0 239H700" stroke="black" stroke-dasharray="4 4"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M0 160H700" stroke="url(#paint10_linear_2862_278)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M511 -1L189 321" stroke="url(#paint11_linear_2862_278)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.1">
<path d="M511 321L189 -1" stroke="url(#paint12_linear_2862_278)"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.05">
<circle cx="350" cy="160" r="111" stroke="black"/>
</g>
<g style="mix-blend-mode:overlay" opacity="0.05">
<circle cx="350" cy="160" r="79" stroke="black"/>
</g>
</g>
<defs>
<filter id="filter0_f_2862_278" x="260" y="-460" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_278"/>
</filter>
<filter id="filter1_f_2862_278" x="-380" y="-187" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_278"/>
</filter>
<filter id="filter2_f_2862_278" x="260" y="80" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_278"/>
</filter>
<filter id="filter3_f_2862_278" x="-380" y="13" width="800" height="800" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="120" result="effect1_foregroundBlur_2862_278"/>
</filter>
<linearGradient id="paint0_linear_2862_278" x1="1.04308e-05" y1="320" x2="710.784" y2="26.0793" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299" stop-opacity="0.09"/>
<stop offset="0.729167" stop-color="#0D9373" stop-opacity="0.08"/>
</linearGradient>
<radialGradient id="paint1_radial_2862_278" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(208.697 189.703) rotate(-10.029) scale(169.097 167.466)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint2_linear_2862_278" x1="306.587" y1="93.5598" x2="252.341" y2="224.228" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint3_linear_2862_278" x1="311.84" y1="123.717" x2="253.579" y2="224.761" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint4_radial_2862_278" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(313.407 243.64) rotate(-75.7542) scale(203.632 223.902)">
<stop stop-color="#00BBBB"/>
<stop offset="0.712616" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint5_linear_2862_278" x1="308.586" y1="102.284" x2="383.487" y2="201.169" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint6_radial_2862_278" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(311.447 249.925) rotate(-20.3524) scale(174.776 163.096)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint7_linear_2862_278" x1="395.843" y1="169.781" x2="332.121" y2="263.82" gradientUnits="userSpaceOnUse">
<stop stop-color="#00B1BC"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint8_linear_2862_278" x1="395.843" y1="169.781" x2="370.991" y2="271.799" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="paint9_linear_2862_278" x1="350" y1="0" x2="350" y2="320" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0"/>
<stop offset="0.0001" stop-opacity="0.3"/>
<stop offset="0.333333"/>
<stop offset="0.666667"/>
<stop offset="1" stop-opacity="0.3"/>
</linearGradient>
<linearGradient id="paint10_linear_2862_278" x1="0" y1="160" x2="700" y2="160" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.1"/>
<stop offset="0.5"/>
<stop offset="1" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint11_linear_2862_278" x1="511" y1="-1" x2="189" y2="321" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.1"/>
<stop offset="0.5"/>
<stop offset="1" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint12_linear_2862_278" x1="511" y1="321" x2="189" y2="-0.999997" gradientUnits="userSpaceOnUse">
<stop stop-opacity="0.1"/>
<stop offset="0.5"/>
<stop offset="1" stop-opacity="0.1"/>
</linearGradient>
<clipPath id="clip0_2862_278">
<rect width="700" height="320" rx="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,55 +0,0 @@
<svg width="160" height="24" viewBox="0 0 160 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="white"/>
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="url(#paint0_radial_115_109)"/>
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="black" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="url(#paint1_linear_115_109)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M7.9354 21.1112C4.89702 21.0957 2.27411 19.4306 1.01347 16.279C-0.248375 13.1244 0.135612 9.18218 1.76165 5.64327C4.80004 5.65882 7.42295 7.32385 8.68359 10.4755C9.94543 13.63 9.56144 17.5723 7.9354 21.1112Z" stroke="url(#paint2_linear_115_109)" stroke-opacity="0.05" stroke-width="0.056338"/>
<path d="M7.31038 21.2574C11.3543 20.2215 14.8836 17.3754 16.6285 13.2361C18.3735 9.09671 17.9448 4.58749 15.8598 0.976291C11.8159 2.01214 8.2866 4.85826 6.54167 8.99762C4.79674 13.137 5.2254 17.6462 7.31038 21.2574Z" fill="white"/>
<path d="M7.31038 21.2574C11.3543 20.2215 14.8836 17.3754 16.6285 13.2361C18.3735 9.09671 17.9448 4.58749 15.8598 0.976291C11.8159 2.01214 8.2866 4.85826 6.54167 8.99762C4.79674 13.137 5.2254 17.6462 7.31038 21.2574Z" fill="url(#paint3_radial_115_109)"/>
<path d="M16.6025 13.2251C14.8642 17.349 11.3512 20.1866 7.32411 21.2248C5.25257 17.624 4.82926 13.1324 6.56764 9.00855C8.30603 4.88472 11.819 2.04706 15.8461 1.00889C17.9176 4.60967 18.3409 9.10131 16.6025 13.2251Z" stroke="url(#paint4_linear_115_109)" stroke-opacity="0.05" stroke-width="0.056338"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="white"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="url(#paint5_radial_115_109)"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="black" fill-opacity="0.2" style="mix-blend-mode:hard-light"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="url(#paint6_linear_115_109)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M16.5682 22.7874C13.2176 23.9184 9.81361 23.2124 7.2672 21.1975C8.49194 17.9068 11.0444 15.189 14.3959 14.0577C17.7465 12.9266 21.1504 13.6326 23.6968 15.6476C22.4721 18.9383 19.9196 21.656 16.5682 22.7874Z" stroke="url(#paint7_linear_115_109)" stroke-opacity="0.05" stroke-width="0.056338"/>
<path d="M34.2124 19V5.4H39.4924L41.6924 12.2L42.3724 14.74L43.0524 12.2L45.2524 5.4H50.4124V19H46.3324L46.5924 9.98L45.5324 13.68L43.7924 19H40.8324L39.0524 13.6L38.0324 10.02L38.2924 19H34.2124ZM52.4155 7.3V4.6H56.2955V7.3H52.4155ZM52.4155 19V8.14H56.2955V19H52.4155ZM58.1038 19V8.14H61.9838V9.58C62.6638 8.34 63.7438 7.76 65.0038 7.76C66.9638 7.76 68.6238 8.98 68.6238 11.78V19H64.7438V12.56C64.7438 11.34 64.3038 10.86 63.4838 10.86C62.6038 10.86 61.9838 11.58 61.9838 12.88V19H58.1038ZM70.9327 15.22V11.06H69.7327V8.14H70.9327V5.62H74.8127V8.14H76.9327V11.06H74.8127V14.6C74.8127 15.5 75.0327 16.06 76.2127 16.06H76.9327V19C76.4927 19.2 75.6727 19.38 74.6527 19.38C72.1527 19.38 70.9327 17.88 70.9327 15.22Z" fill="url(#paint8_radial_115_109)"/>
<path d="M87.232 10.519C87.232 13.687 94.1125 11.2285 94.1125 15.832C94.1125 17.9935 92.3635 19.198 89.971 19.198C87.562 19.198 85.912 18.0925 85.417 15.832H87.001C87.364 17.1685 88.3705 17.8945 89.9875 17.8945C91.6705 17.8945 92.5615 17.152 92.5615 16.03C92.5615 12.598 85.681 15.1555 85.681 10.618C85.681 9.001 87.034 7.582 89.509 7.582C91.6705 7.582 93.403 8.6215 93.8155 11.014H92.215C91.8685 9.529 90.9115 8.8855 89.476 8.8855C88.057 8.8855 87.232 9.529 87.232 10.519ZM96.2499 16.4755V11.3935H95.0289V10.2385H96.2499V8.2255H97.7019V10.2385H99.6324V11.3935H97.7019V16.4755C97.7019 17.5315 98.0154 18.0265 99.3024 18.0265H99.5994V19.066C99.4344 19.1485 99.0714 19.198 98.6589 19.198C97.0254 19.198 96.2499 18.3235 96.2499 16.4755ZM102.516 13.093H101.064C101.345 11.1625 102.615 10.024 104.76 10.024C107.103 10.024 108.242 11.3935 108.242 13.4395V16.888C108.242 17.8945 108.324 18.5215 108.555 19H107.021C106.856 18.6535 106.806 18.142 106.79 17.614C106.047 18.7195 104.859 19.198 103.803 19.198C101.988 19.198 100.767 18.3565 100.767 16.69C100.767 15.4855 101.427 14.611 102.714 14.182C103.902 13.786 105.107 13.687 106.79 13.6705V13.4725C106.79 12.0535 106.13 11.278 104.628 11.278C103.374 11.278 102.698 11.971 102.516 13.093ZM102.252 16.657C102.252 17.4655 102.929 17.944 103.952 17.944C105.569 17.944 106.79 16.6735 106.79 15.172V14.7595C103.061 14.7925 102.252 15.5845 102.252 16.657ZM110.787 19V10.2385H112.239V11.5915C112.833 10.519 113.774 10.024 114.83 10.024C115.176 10.024 115.49 10.1065 115.655 10.2385V11.542C115.407 11.4595 115.094 11.4265 114.747 11.4265C112.998 11.4265 112.239 12.5155 112.239 14.0995V19H110.787ZM117.305 16.4755V11.3935H116.084V10.2385H117.305V8.2255H118.757V10.2385H120.688V11.3935H118.757V16.4755C118.757 17.5315 119.071 18.0265 120.358 18.0265H120.655V19.066C120.49 19.1485 120.127 19.198 119.714 19.198C118.081 19.198 117.305 18.3235 117.305 16.4755ZM129.809 16.1455C129.33 18.1915 127.862 19.198 125.865 19.198C123.324 19.198 121.79 17.482 121.79 14.6275C121.79 11.6575 123.324 10.024 125.783 10.024C128.258 10.024 129.743 11.7235 129.743 14.512V14.875H123.275C123.357 16.8385 124.281 17.944 125.865 17.944C127.103 17.944 127.977 17.35 128.291 16.1455H129.809ZM125.783 11.278C124.38 11.278 123.539 12.1525 123.324 13.786H128.225C128.027 12.169 127.152 11.278 125.783 11.278ZM131.843 19V10.2385H133.295V11.5915C133.889 10.519 134.829 10.024 135.885 10.024C136.232 10.024 136.545 10.1065 136.71 10.2385V11.542C136.463 11.4595 136.149 11.4265 135.803 11.4265C134.054 11.4265 133.295 12.5155 133.295 14.0995V19H131.843ZM141.763 19V7.78H143.281V13.192L148.413 7.78H150.327L145.047 13.291L150.459 19H148.413L143.281 13.621V19H141.763ZM152.06 9.067V7.12H153.512V9.067H152.06ZM152.06 19V10.2385H153.512V19H152.06ZM156.178 16.4755V11.3935H154.957V10.2385H156.178V8.2255H157.63V10.2385H159.56V11.3935H157.63V16.4755C157.63 17.5315 157.943 18.0265 159.23 18.0265H159.527V19.066C159.362 19.1485 158.999 19.198 158.587 19.198C156.953 19.198 156.178 18.3235 156.178 16.4755Z" fill="white" fill-opacity="0.55"/>
<defs>
<radialGradient id="paint0_radial_115_109" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-3.00503 15.023) rotate(-10.029) scale(17.9572 17.784)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint1_linear_115_109" x1="7.39036" y1="4.81308" x2="1.62975" y2="18.6894" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint2_linear_115_109" x1="7.94816" y1="8.01562" x2="1.7612" y2="18.746" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint3_radial_115_109" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.11404 20.8822) rotate(-75.7542) scale(21.6246 23.7772)">
<stop stop-color="#00BBBB"/>
<stop offset="0.712616" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint4_linear_115_109" x1="7.60205" y1="5.8709" x2="15.5561" y2="16.3719" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint5_radial_115_109" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.84537 21.5181) rotate(-20.3525) scale(18.5603 17.32)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint6_linear_115_109" x1="16.8078" y1="13.0071" x2="10.0409" y2="22.9937" gradientUnits="userSpaceOnUse">
<stop stop-color="#00B1BC"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint7_linear_115_109" x1="16.8078" y1="13.0071" x2="14.1687" y2="23.841" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint8_radial_115_109" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(47.2781 7) rotate(19.0047) scale(67.5582 85.7506)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.5"/>
</radialGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,51 +0,0 @@
<svg width="160" height="24" viewBox="0 0 160 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="white"/>
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="url(#paint0_radial_115_86)"/>
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="black" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M7.95343 21.1394C4.89586 21.1304 2.25471 19.458 0.987296 16.2895C-0.280118 13.121 0.108924 9.16314 1.74363 5.61504C4.8012 5.62409 7.44235 7.29648 8.70976 10.465C9.97718 13.6335 9.58814 17.5913 7.95343 21.1394Z" fill="url(#paint1_linear_115_86)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M7.9354 21.1112C4.89702 21.0957 2.27411 19.4306 1.01347 16.279C-0.248375 13.1244 0.135612 9.18218 1.76165 5.64327C4.80004 5.65882 7.42295 7.32385 8.68359 10.4755C9.94543 13.63 9.56144 17.5723 7.9354 21.1112Z" stroke="url(#paint2_linear_115_86)" stroke-opacity="0.05" stroke-width="0.056338"/>
<path d="M7.31038 21.2574C11.3543 20.2215 14.8836 17.3754 16.6285 13.2361C18.3735 9.09671 17.9448 4.58749 15.8598 0.976291C11.8159 2.01214 8.2866 4.85826 6.54167 8.99762C4.79674 13.137 5.2254 17.6462 7.31038 21.2574Z" fill="white"/>
<path d="M7.31038 21.2574C11.3543 20.2215 14.8836 17.3754 16.6285 13.2361C18.3735 9.09671 17.9448 4.58749 15.8598 0.976291C11.8159 2.01214 8.2866 4.85826 6.54167 8.99762C4.79674 13.137 5.2254 17.6462 7.31038 21.2574Z" fill="url(#paint3_radial_115_86)"/>
<path d="M16.6025 13.2251C14.8642 17.349 11.3512 20.1866 7.32411 21.2248C5.25257 17.624 4.82926 13.1324 6.56764 9.00855C8.30603 4.88472 11.819 2.04706 15.8461 1.00889C17.9176 4.60967 18.3409 9.10131 16.6025 13.2251Z" stroke="url(#paint4_linear_115_86)" stroke-opacity="0.05" stroke-width="0.056338"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="white"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="url(#paint5_radial_115_86)"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="black" fill-opacity="0.2" style="mix-blend-mode:hard-light"/>
<path d="M7.23368 21.2069C9.78906 23.2373 13.2102 23.9506 16.5772 22.8141C19.9441 21.6775 22.5058 18.9445 23.7304 15.6382C21.175 13.6078 17.7538 12.8944 14.3869 14.031C11.0199 15.1676 8.45822 17.9006 7.23368 21.2069Z" fill="url(#paint6_linear_115_86)" fill-opacity="0.5" style="mix-blend-mode:hard-light"/>
<path d="M16.5682 22.7874C13.2176 23.9184 9.81361 23.2124 7.2672 21.1975C8.49194 17.9068 11.0444 15.189 14.3959 14.0577C17.7465 12.9266 21.1504 13.6326 23.6968 15.6476C22.4721 18.9383 19.9196 21.656 16.5682 22.7874Z" stroke="url(#paint7_linear_115_86)" stroke-opacity="0.05" stroke-width="0.056338"/>
<path d="M34.2124 19V5.4H39.4924L41.6924 12.2L42.3724 14.74L43.0524 12.2L45.2524 5.4H50.4124V19H46.3324L46.5924 9.98L45.5324 13.68L43.7924 19H40.8324L39.0524 13.6L38.0324 10.02L38.2924 19H34.2124ZM52.4155 7.3V4.6H56.2955V7.3H52.4155ZM52.4155 19V8.14H56.2955V19H52.4155ZM58.1038 19V8.14H61.9838V9.58C62.6638 8.34 63.7438 7.76 65.0038 7.76C66.9638 7.76 68.6238 8.98 68.6238 11.78V19H64.7438V12.56C64.7438 11.34 64.3038 10.86 63.4838 10.86C62.6038 10.86 61.9838 11.58 61.9838 12.88V19H58.1038ZM70.9327 15.22V11.06H69.7327V8.14H70.9327V5.62H74.8127V8.14H76.9327V11.06H74.8127V14.6C74.8127 15.5 75.0327 16.06 76.2127 16.06H76.9327V19C76.4927 19.2 75.6727 19.38 74.6527 19.38C72.1527 19.38 70.9327 17.88 70.9327 15.22Z" fill="#001E13"/>
<path d="M87.232 10.519C87.232 13.687 94.1125 11.2285 94.1125 15.832C94.1125 17.9935 92.3635 19.198 89.971 19.198C87.562 19.198 85.912 18.0925 85.417 15.832H87.001C87.364 17.1685 88.3705 17.8945 89.9875 17.8945C91.6705 17.8945 92.5615 17.152 92.5615 16.03C92.5615 12.598 85.681 15.1555 85.681 10.618C85.681 9.001 87.034 7.582 89.509 7.582C91.6705 7.582 93.403 8.6215 93.8155 11.014H92.215C91.8685 9.529 90.9115 8.8855 89.476 8.8855C88.057 8.8855 87.232 9.529 87.232 10.519ZM96.2499 16.4755V11.3935H95.0289V10.2385H96.2499V8.2255H97.7019V10.2385H99.6324V11.3935H97.7019V16.4755C97.7019 17.5315 98.0154 18.0265 99.3024 18.0265H99.5994V19.066C99.4344 19.1485 99.0714 19.198 98.6589 19.198C97.0254 19.198 96.2499 18.3235 96.2499 16.4755ZM102.516 13.093H101.064C101.345 11.1625 102.615 10.024 104.76 10.024C107.103 10.024 108.242 11.3935 108.242 13.4395V16.888C108.242 17.8945 108.324 18.5215 108.555 19H107.021C106.856 18.6535 106.806 18.142 106.79 17.614C106.047 18.7195 104.859 19.198 103.803 19.198C101.988 19.198 100.767 18.3565 100.767 16.69C100.767 15.4855 101.427 14.611 102.714 14.182C103.902 13.786 105.107 13.687 106.79 13.6705V13.4725C106.79 12.0535 106.13 11.278 104.628 11.278C103.374 11.278 102.698 11.971 102.516 13.093ZM102.252 16.657C102.252 17.4655 102.929 17.944 103.952 17.944C105.569 17.944 106.79 16.6735 106.79 15.172V14.7595C103.061 14.7925 102.252 15.5845 102.252 16.657ZM110.787 19V10.2385H112.239V11.5915C112.833 10.519 113.774 10.024 114.83 10.024C115.176 10.024 115.49 10.1065 115.655 10.2385V11.542C115.407 11.4595 115.094 11.4265 114.747 11.4265C112.998 11.4265 112.239 12.5155 112.239 14.0995V19H110.787ZM117.305 16.4755V11.3935H116.084V10.2385H117.305V8.2255H118.757V10.2385H120.688V11.3935H118.757V16.4755C118.757 17.5315 119.071 18.0265 120.358 18.0265H120.655V19.066C120.49 19.1485 120.127 19.198 119.714 19.198C118.081 19.198 117.305 18.3235 117.305 16.4755ZM129.809 16.1455C129.33 18.1915 127.862 19.198 125.865 19.198C123.324 19.198 121.79 17.482 121.79 14.6275C121.79 11.6575 123.324 10.024 125.783 10.024C128.258 10.024 129.743 11.7235 129.743 14.512V14.875H123.275C123.357 16.8385 124.281 17.944 125.865 17.944C127.103 17.944 127.977 17.35 128.291 16.1455H129.809ZM125.783 11.278C124.38 11.278 123.539 12.1525 123.324 13.786H128.225C128.027 12.169 127.152 11.278 125.783 11.278ZM131.843 19V10.2385H133.295V11.5915C133.889 10.519 134.829 10.024 135.885 10.024C136.232 10.024 136.545 10.1065 136.71 10.2385V11.542C136.463 11.4595 136.149 11.4265 135.803 11.4265C134.054 11.4265 133.295 12.5155 133.295 14.0995V19H131.843ZM141.763 19V7.78H143.281V13.192L148.413 7.78H150.327L145.047 13.291L150.459 19H148.413L143.281 13.621V19H141.763ZM152.06 9.067V7.12H153.512V9.067H152.06ZM152.06 19V10.2385H153.512V19H152.06ZM156.178 16.4755V11.3935H154.957V10.2385H156.178V8.2255H157.63V10.2385H159.56V11.3935H157.63V16.4755C157.63 17.5315 157.943 18.0265 159.23 18.0265H159.527V19.066C159.362 19.1485 158.999 19.198 158.587 19.198C156.953 19.198 156.178 18.3235 156.178 16.4755Z" fill="#002719" fill-opacity="0.6"/>
<defs>
<radialGradient id="paint0_radial_115_86" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-3.00503 15.023) rotate(-10.029) scale(17.9572 17.784)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint1_linear_115_86" x1="7.39036" y1="4.81308" x2="1.62975" y2="18.6894" gradientUnits="userSpaceOnUse">
<stop stop-color="#18E299"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint2_linear_115_86" x1="7.94816" y1="8.01562" x2="1.7612" y2="18.746" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint3_radial_115_86" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(8.11404 20.8822) rotate(-75.7542) scale(21.6246 23.7772)">
<stop stop-color="#00BBBB"/>
<stop offset="0.712616" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint4_linear_115_86" x1="7.60205" y1="5.8709" x2="15.5561" y2="16.3719" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<radialGradient id="paint5_radial_115_86" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(7.84537 21.5181) rotate(-20.3525) scale(18.5603 17.32)">
<stop stop-color="#00B0BB"/>
<stop offset="1" stop-color="#00DB65"/>
</radialGradient>
<linearGradient id="paint6_linear_115_86" x1="16.8078" y1="13.0071" x2="10.0409" y2="22.9937" gradientUnits="userSpaceOnUse">
<stop stop-color="#00B1BC"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint7_linear_115_86" x1="16.8078" y1="13.0071" x2="14.1687" y2="23.841" gradientUnits="userSpaceOnUse">
<stop/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -1,4 +0,0 @@
---
title: 'Login (password)'
openapi: 'POST /api/auth/password/login'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Me'
openapi: 'GET /api/auth/me'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Register (password)'
openapi: 'POST /api/auth/password/register'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Strategies'
openapi: 'GET /api/auth/strategies'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Create Entity'
openapi: 'POST /api/data/entity/{entity}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Delete Entity'
openapi: 'DELETE /api/data/entity/{entity}/{id}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Get Entity'
openapi: 'GET /api/data/entity/{entity}/{id}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'List Entity'
openapi: 'GET /api/data/entity/{entity}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Update Entity'
openapi: 'PATCH /api/data/entity/{entity}/{id}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Create Plant'
openapi: 'POST /plants'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Delete Plant'
openapi: 'DELETE /plants/{id}'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Get Plants'
openapi: 'GET /plants'
---

View File

@@ -1,391 +0,0 @@
{
"openapi": "3.1.0",
"info": { "title": "bknd API", "version": "0.0.0" },
"paths": {
"/api/system/ping": {
"get": {
"summary": "Ping",
"responses": {
"200": {
"description": "Pong",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"pong": { "default": true, "type": "boolean" }
},
"required": ["pong"]
}
}
}
}
},
"tags": ["system"]
}
},
"/api/system/config": {
"get": {
"summary": "Get config",
"responses": {
"200": {
"description": "Config",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": { "type": "number" },
"server": { "type": "object", "properties": {} },
"data": { "type": "object", "properties": {} },
"auth": { "type": "object", "properties": {} },
"flows": { "type": "object", "properties": {} },
"media": { "type": "object", "properties": {} }
},
"required": [
"version",
"server",
"data",
"auth",
"flows",
"media"
]
}
}
}
}
},
"tags": ["system"]
}
},
"/api/system/schema": {
"get": {
"summary": "Get config",
"responses": {
"200": {
"description": "Config",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"version": { "type": "number" },
"schema": {
"type": "object",
"properties": {
"server": { "type": "object", "properties": {} },
"data": { "type": "object", "properties": {} },
"auth": { "type": "object", "properties": {} },
"flows": { "type": "object", "properties": {} },
"media": { "type": "object", "properties": {} }
},
"required": ["server", "data", "auth", "flows", "media"]
}
},
"required": ["version", "schema"]
}
}
}
}
},
"tags": ["system"]
}
},
"/api/data/entity/{entity}": {
"get": {
"summary": "List entities",
"parameters": [
{
"name": "entity",
"in": "path",
"required": true,
"schema": { "type": "string" }
}
],
"responses": {
"200": {
"description": "List of entities",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"properties": { "id": { "type": "number" } },
"required": ["id"]
}
}
}
}
}
},
"tags": ["data"]
},
"post": {
"summary": "Create entity",
"parameters": [
{
"name": "entity",
"in": "path",
"required": true,
"schema": { "type": "string" }
}
],
"requestBody": {
"content": {
"application/json": {
"schema": { "type": "object", "properties": {} }
}
}
},
"responses": {
"200": {
"description": "Entity",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": { "id": { "type": "number" } },
"required": ["id"]
}
}
}
}
},
"tags": ["data"]
}
},
"/api/data/entity/{entity}/{id}": {
"get": {
"summary": "Get entity",
"parameters": [
{
"name": "entity",
"in": "path",
"required": true,
"schema": { "type": "string" }
},
{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "number" }
}
],
"responses": {
"200": {
"description": "Entity",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": { "id": { "type": "number" } },
"required": ["id"]
}
}
}
}
},
"tags": ["data"]
},
"patch": {
"summary": "Update entity",
"parameters": [
{
"name": "entity",
"in": "path",
"required": true,
"schema": { "type": "string" }
},
{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "number" }
}
],
"requestBody": {
"content": {
"application/json": {
"schema": { "type": "object", "properties": {} }
}
}
},
"responses": {
"200": {
"description": "Entity",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": { "id": { "type": "number" } },
"required": ["id"]
}
}
}
}
},
"tags": ["data"]
},
"delete": {
"summary": "Delete entity",
"parameters": [
{
"name": "entity",
"in": "path",
"required": true,
"schema": { "type": "string" }
},
{
"name": "id",
"in": "path",
"required": true,
"schema": { "type": "number" }
}
],
"responses": { "200": { "description": "Entity deleted" } },
"tags": ["data"]
}
},
"/api/auth/password/login": {
"post": {
"summary": "Login",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"email": { "type": "string" },
"password": { "type": "string" }
},
"required": ["email", "password"]
}
}
}
},
"responses": {
"200": {
"description": "User",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": { "type": "string" },
"email": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "email", "name"]
}
},
"required": ["user"]
}
}
}
}
},
"tags": ["auth"]
}
},
"/api/auth/password/register": {
"post": {
"summary": "Register",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"email": { "type": "string" },
"password": { "type": "string" }
},
"required": ["email", "password"]
}
}
}
},
"responses": {
"200": {
"description": "User",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": { "type": "string" },
"email": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "email", "name"]
}
},
"required": ["user"]
}
}
}
}
},
"tags": ["auth"]
}
},
"/api/auth/me": {
"get": {
"summary": "Get me",
"responses": {
"200": {
"description": "User",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": { "type": "string" },
"email": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "email", "name"]
}
},
"required": ["user"]
}
}
}
}
},
"tags": ["auth"]
}
},
"/api/auth/strategies": {
"get": {
"summary": "Get auth strategies",
"responses": {
"200": {
"description": "Strategies",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"strategies": { "type": "object", "properties": {} }
},
"required": ["strategies"]
}
}
}
}
},
"tags": ["auth"]
}
}
}
}

View File

@@ -1,4 +0,0 @@
---
title: 'Config'
openapi: 'GET /api/system/config'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Ping'
openapi: 'GET /api/system/ping'
---

View File

@@ -1,4 +0,0 @@
---
title: 'Schema'
openapi: 'GET /api/system/schema'
---

View File

@@ -0,0 +1,64 @@
import { source } from "@/lib/source";
import { DocsPage, DocsBody, DocsDescription, DocsTitle } from "fumadocs-ui/page";
import { notFound } from "next/navigation";
import { createRelativeLink } from "fumadocs-ui/mdx";
import { getMDXComponents } from "@/mdx-components";
import { LLMCopyButton, ViewOptions } from "@/components/ai/page-actions";
export default async function Page(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDXContent = page.data.body;
return (
<DocsPage
toc={page.data.toc}
full={page.data.full}
tableOfContent={{
style: "clerk",
}}
>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
<div className="flex flex-row gap-2 items-center border-b pt-2 pb-6">
<LLMCopyButton markdownUrl={`${page.url}.mdx`} />
<ViewOptions
markdownUrl={`${page.url}.mdx`}
githubUrl={`https://github.com/bknd-io/bknd/blob/dev/apps/docs/content/docs/${page.path}`}
/>
</div>
<DocsBody>
<MDXContent
components={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: {
params: Promise<{ slug?: string[] }>;
}) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
icons: {
icon: "/favicon.svg",
},
};
}

View File

@@ -0,0 +1,156 @@
"use client";
import { motion } from "motion/react";
import {
ComponentPropsWithoutRef,
useCallback,
useEffect,
useId,
useRef,
useState,
} from "react";
import { twMerge as cn } from "tailwind-merge";
export interface AnimatedGridPatternProps
extends ComponentPropsWithoutRef<"svg"> {
width?: number;
height?: number;
x?: number;
y?: number;
strokeDasharray?: string | number;
numSquares?: number;
maxOpacity?: number;
duration?: number;
repeatDelay?: number;
}
export function AnimatedGridPattern({
width = 40,
height = 40,
x = -1,
y = -1,
strokeDasharray = 0,
numSquares = 50,
className,
maxOpacity = 0.5,
duration = 4,
repeatDelay = 0.5,
...props
}: AnimatedGridPatternProps) {
const id = useId();
const containerRef = useRef<SVGSVGElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const getPos = useCallback(() => {
return [
Math.floor((Math.random() * dimensions.width) / width),
Math.floor((Math.random() * dimensions.height) / height),
];
}, [dimensions.width, dimensions.height, width, height]);
const generateSquares = useCallback(
(count: number) => {
return Array.from({ length: count }, (_, i) => ({
id: i,
pos: getPos(),
}));
},
[getPos],
);
const [squares, setSquares] = useState(() => generateSquares(numSquares));
const updateSquarePosition = (id: number) => {
setSquares((currentSquares) =>
currentSquares.map((sq) =>
sq.id === id
? {
...sq,
pos: getPos(),
}
: sq,
),
);
};
useEffect(() => {
if (dimensions.width && dimensions.height) {
setSquares(generateSquares(numSquares));
}
}, [dimensions, numSquares, generateSquares]);
useEffect(() => {
const current = containerRef.current;
if (!current) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setDimensions({
width: entry.contentRect.width,
height: entry.contentRect.height,
});
}
});
resizeObserver.observe(current);
return () => {
resizeObserver.unobserve(current);
};
}, []);
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
x={x}
y={y}
>
<path
d={`M.5 ${height}V.5H${width}`}
fill="none"
stroke="currentColor"
strokeDasharray={strokeDasharray}
/>
</pattern>
</defs>
<rect width="100%" height="100%" fill={`url(#${id})`} />
<svg x={x} y={y} className="overflow-visible">
{squares.map(({ pos: [x, y], id }, index) => (
<motion.rect
key={`${x}-${y}-${index}`}
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatDelay,
repeatType: "reverse",
}}
onAnimationComplete={() => updateSquarePosition(id)}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
fill="currentColor"
strokeWidth="0"
/>
))}
</svg>
</svg>
);
}

View File

@@ -0,0 +1,29 @@
import type { ReactNode } from "react";
export function CalloutCaution({
title,
children,
}: {
title?: string;
children: ReactNode;
}) {
return (
<div className="rounded-xl my-4 px-4 py-3 flex gap-3 border bg-yellow-50 border-yellow-200 text-yellow-900 dark:bg-yellow-900/15 dark:border-yellow-900 dark:text-yellow-100 [&>div>p]:m-0">
<div className="pt-1.5 text-yellow-500 dark:text-yellow-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="-2 -2 24 24"
fill="currentColor"
>
<path d="M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0-13a1 1 0 0 1 1 1v5a1 1 0 0 1-2 0V6a1 1 0 0 1 1-1m0 10a1 1 0 1 1 0-2a1 1 0 0 1 0 2" />
</svg>
</div>
<div>
{title && <span className="font-semibold">{title}</span>}
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import type { ReactNode } from "react";
export function CalloutDanger({
title,
children,
}: {
title?: string;
children: ReactNode;
}) {
return (
<div className="rounded-xl my-4 px-4 py-3 flex gap-3 border bg-red-50 border-red-200 text-red-900 dark:bg-red-900/15 dark:border-red-900 dark:text-red-100 [&>div>p]:m-0">
<div className="pt-1.5 text-red-500 dark:text-red-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
>
<g>
<path d="M16.34 9.322a1 1 0 1 0-1.364-1.463l-2.926 2.728L9.322 7.66A1 1 0 0 0 7.86 9.024l2.728 2.926l-2.927 2.728a1 1 0 1 0 1.364 1.462l2.926-2.727l2.728 2.926a1 1 0 1 0 1.462-1.363l-2.727-2.926z" />
<path
fillRule="evenodd"
d="M1 12C1 5.925 5.925 1 12 1s11 4.925 11 11s-4.925 11-11 11S1 18.075 1 12m11 9a9 9 0 1 1 0-18a9 9 0 0 1 0 18"
clipRule="evenodd"
/>
</g>
</svg>
</div>
<div>
{title && <span className="font-semibold">{title}</span>}
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,29 @@
import type { ReactNode } from "react";
export function CalloutInfo({
title,
children,
}: {
title?: string;
children: ReactNode;
}) {
return (
<div className="rounded-xl my-4 px-4 py-3 flex gap-3 border bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-900/15 dark:border-blue-900 dark:text-blue-100 [&>div>p]:m-0">
<div className="pt-1.5 text-blue-500 dark:text-blue-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width={18}
height={18}
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M11 9h2V7h-2m1 13c-4.41 0-8-3.59-8-8s3.59-8 8-8s8 3.59 8 8s-3.59 8-8 8m0-18A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2m-1 15h2v-6h-2z" />
</svg>
</div>
<div>
{title && <span className="font-semibold">{title}</span>}
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import type { ReactNode } from "react";
export function CalloutPositive({
title,
children,
}: {
title?: string;
children: ReactNode;
}) {
return (
<div className="rounded-xl my-4 px-4 py-3 flex gap-3 border bg-green-50 border-green-200 text-green-900 dark:bg-green-900/15 dark:border-green-900 dark:text-green-100 [&>div>p]:m-0">
<div className="pt-1.5 text-green-500 dark:text-green-100">
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
>
<g fill="none" fillRule="evenodd">
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path
fill="currentColor"
d="M21.546 5.111a1.5 1.5 0 0 1 0 2.121L10.303 18.475a1.6 1.6 0 0 1-2.263 0L2.454 12.89a1.5 1.5 0 1 1 2.121-2.121l4.596 4.596L19.424 5.111a1.5 1.5 0 0 1 2.122 0"
/>
</g>
</svg>
</div>
<div>
{title && <span className="font-semibold">{title}</span>}
{children}
</div>
</div>
);
}

View File

@@ -0,0 +1,4 @@
export { CalloutPositive } from "./CalloutPositive";
export { CalloutCaution } from "./CalloutCaution";
export { CalloutDanger } from "./CalloutDanger";
export { CalloutInfo } from "./CalloutInfo";

View File

@@ -0,0 +1,48 @@
import { ThemeToggle } from "fumadocs-ui/components/layout/theme-toggle";
export function FooterIcons() {
return (
<div className="flex justify-between items-center w-full px-2">
<div className="flex items-center gap-3">
<a
href="https://github.com/bknd-io/bknd"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-fd-muted-foreground hover:text-fd-accent-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
></path>
</svg>
</a>
<a
href="https://discord.gg/952SFk8Tb8"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="text-fd-muted-foreground hover:text-fd-accent-foreground"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="w-6.5 h-6.5"
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M18.942 5.556a16.3 16.3 0 0 0-4.126-1.3a12 12 0 0 0-.529 1.1a15.2 15.2 0 0 0-4.573 0a12 12 0 0 0-.535-1.1a16.3 16.3 0 0 0-4.129 1.3a17.4 17.4 0 0 0-2.868 11.662a15.8 15.8 0 0 0 4.963 2.521q.616-.847 1.084-1.785a10.6 10.6 0 0 1-1.706-.83q.215-.16.418-.331a11.66 11.66 0 0 0 10.118 0q.206.172.418.331q-.817.492-1.71.832a12.6 12.6 0 0 0 1.084 1.785a16.5 16.5 0 0 0 5.064-2.595a17.3 17.3 0 0 0-2.973-11.59M8.678 14.813a1.94 1.94 0 0 1-1.8-2.045a1.93 1.93 0 0 1 1.8-2.047a1.92 1.92 0 0 1 1.8 2.047a1.93 1.93 0 0 1-1.8 2.045m6.644 0a1.94 1.94 0 0 1-1.8-2.045a1.93 1.93 0 0 1 1.8-2.047a1.92 1.92 0 0 1 1.8 2.047a1.93 1.93 0 0 1-1.8 2.045"
></path>
</svg>
</a>
</div>
<ThemeToggle className="p-0" />
</div>
);
}

View File

@@ -0,0 +1,24 @@
import Image from "next/image";
export function Logo() {
return (
<>
<Image
src="/logo/bknd_logo_white.svg"
alt="bknd logo"
width={110}
height={24}
className="hidden dark:block pl-1.5"
priority
/>
<Image
src="/logo/bknd_logo_black.svg"
alt="bknd logo"
width={110}
height={24}
className="block dark:hidden pl-1.5"
priority
/>
</>
);
}

View File

@@ -0,0 +1,67 @@
"use client";
import { create } from "@orama/orama";
import { useDocsSearch } from "fumadocs-core/search/client";
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogFooter,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from "fumadocs-ui/components/dialog/search";
import { useI18n } from "fumadocs-ui/contexts/i18n";
function initOrama() {
return create({
schema: { _: "string" },
// https://docs.orama.com/open-source/supported-languages
language: "english",
});
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
type: "static",
initOrama,
locale,
from: "/api/search",
});
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== "empty" ? query.data : null} />
<SearchDialogFooter>
<div className="flex w-full">
<a
href="https://orama.com"
rel="noreferrer noopener"
className="ml-auto text-xs text-nowrap text-fd-muted-foreground"
>
Powered by Orama
</a>
</div>
</SearchDialogFooter>
</SearchDialogContent>
</SearchDialog>
);
}

View File

@@ -0,0 +1,138 @@
"use client";
import { create } from "@orama/orama";
import { clsx } from "clsx";
import { useDocsSearch } from "fumadocs-core/search/client";
import {
SearchDialog,
SearchDialogClose,
SearchDialogContent,
SearchDialogFooter,
SearchDialogHeader,
SearchDialogIcon,
SearchDialogInput,
SearchDialogList,
SearchDialogOverlay,
type SharedProps,
} from "fumadocs-ui/components/dialog/search";
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "fumadocs-ui/components/ui/popover";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
function initOrama() {
return create({
schema: {
_: "string",
title: "string",
description: "string",
url: "string",
tags: "string[]",
},
});
}
const tagItems = [
{
name: "All",
value: undefined,
},
{
name: "Documentation",
description: "Developer documentation",
value: "documentation",
},
{
name: "Guide",
description: "User Guide",
value: "guide",
},
{
name: "OpenAPI",
description: "OpenAPI Reference",
value: "openapi",
},
];
export default function DefaultSearchDialog(props: SharedProps) {
const [tag, setTag] = useState<string>();
const [open, setOpen] = useState(false);
const { search, setSearch, query } = useDocsSearch({
type: "static",
initOrama,
tag,
from: "/api/search",
});
return (
<SearchDialog
search={search}
onSearchChange={setSearch}
isLoading={query.isLoading}
{...props}
>
<SearchDialogOverlay />
<SearchDialogContent>
<SearchDialogHeader>
<SearchDialogIcon />
<SearchDialogInput />
<SearchDialogClose />
</SearchDialogHeader>
<SearchDialogList items={query.data !== "empty" ? query.data : null} />
<SearchDialogFooter className="flex flex-row flex-wrap gap-2 items-center">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
className={buttonVariants({
size: "sm",
color: "ghost",
className: "-m-1.5 me-auto",
})}
>
<span className="text-fd-muted-foreground/80 me-2">Filter</span>
{tagItems.find((item) => item.value === tag)?.name ?? "All"}
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
</PopoverTrigger>
<PopoverContent className="flex flex-col p-1 gap-1" align="start">
{tagItems.map((item, i) => {
const isSelected = item.value === tag;
return (
<button
key={i}
onClick={() => {
setTag(item.value);
setOpen(false);
}}
className={clsx(
"rounded-lg text-start px-2 py-1.5",
isSelected
? "text-fd-primary bg-fd-primary/10"
: "hover:text-fd-accent-foreground hover:bg-fd-accent",
)}
>
<p className="font-medium mb-0.5">{item.name}</p>
<p className="text-xs opacity-70">{item.description}</p>
</button>
);
})}
</PopoverContent>
</Popover>
<a
href="https://orama.com"
rel="noreferrer noopener"
className="text-xs text-nowrap text-fd-muted-foreground"
>
Powered by Orama
</a>
</SearchDialogFooter>
</SearchDialogContent>
</SearchDialog>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import * as React from "react";
export const examples = {
adminRich: {
path: "github/bknd-io/bknd-examples",
startScript: "example-admin-rich",
initialPath: "/data/schema"
}
};
export const StackBlitz = ({
path,
ratio = 9 / 16,
example,
...props
}: {
path?: string;
ratio?: number;
example?: keyof typeof examples;
[key: string]: unknown;
}) => {
const selected = example ? examples[example] : undefined;
const finalPath = path || selected?.path || "github/bknd-io/bknd-examples";
const params = new URLSearchParams({
ctl: "1",
hideExplorer: "1",
embed: "1",
view: "preview",
...(selected || {}),
...props
});
const url = new URL(
`https://stackblitz.com/${finalPath}?${params.toString()}`
);
return (
<>
<div
style={{
position: "relative",
overflow: "hidden",
width: "100%",
paddingTop: `${ratio * 100}%`
}}
>
<iframe
width="100%"
height="100%"
src={url.toString()}
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
right: 0,
border: "none"
}}
/>
</div>
<div
style={{
fontSize: "80%",
opacity: 0.7,
marginTop: "0.2rem",
marginBottom: "1rem",
textAlign: "center"
}}
>
If you&rsquo;re having issues viewing it inline,{" "}
<a href={url.toString()} target="_blank" rel="noreferrer">
try in a new tab
</a>
.
</div>
</>
);
};

View File

@@ -0,0 +1,5 @@
import { source } from "@/lib/source";
import { createFromSource } from "fumadocs-core/search/server";
export const revalidate = false;
export const { staticGET: GET } = createFromSource(source);

19
docs/app/global.css Normal file
View File

@@ -0,0 +1,19 @@
@import "tailwindcss";
@import "fumadocs-ui/css/neutral.css";
@import "fumadocs-ui/css/preset.css";
@import "fumadocs-openapi/css/preset.css";
@import "fumadocs-twoslash/twoslash.css";
:root {
--color-fd-primary: #008001;
--color-fd-primary-foreground: #000000;
--tw-prose-links: #008001;
--tw-prose-code: #008001;
}
.dark {
--color-fd-primary: #4ec9b0;
--color-fd-primary-foreground: #000000;
--tw-prose-links: #4ec9b0;
--tw-prose-code: #4ec9b0;
}

View File

@@ -0,0 +1,17 @@
import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared";
import { Logo } from "@/app/_components/Logo";
/**
* Shared layout configurations
*
* you can customise layouts individually from:
* Home Layout: app/(home)/layout.tsx
* Docs Layout: app/docs/layout.tsx
*/
export const baseOptions: BaseLayoutProps = {
nav: {
title: <Logo />,
},
// see https://fumadocs.dev/docs/ui/navigation/links
links: [],
};

34
docs/app/layout.tsx Normal file
View File

@@ -0,0 +1,34 @@
import "./global.css";
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import { baseOptions } from "./layout.config";
import { source } from "@/lib/source";
import type { ReactNode } from "react";
import { Provider } from "./provider";
import { AnimatedGridPattern } from "./_components/AnimatedGridPattern";
import { FooterIcons } from "./_components/FooterIcons";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body className="relative bg-background">
<div className="absolute top-0 inset-x-0 h-[300px] -z-10 pointer-events-none [mask-image:linear-gradient(to_bottom,black,transparent)]">
<AnimatedGridPattern className="h-full w-full opacity-15 dark:opacity-10 text-[var(--color-fd-primary)] dark:text-[var(--color-fd-primary)]" />
</div>
<Provider>
<DocsLayout
tree={source.pageTree}
nav={{ ...baseOptions.nav }}
themeSwitch={{
enabled: true,
component: <FooterIcons />,
mode: "light-dark",
}}
>
{children}
</DocsLayout>
</Provider>
</body>
</html>
);
}

View File

@@ -0,0 +1,12 @@
import { source } from "@/lib/source";
import { getLLMText } from "@/lib/get-llm-text";
// cached forever
export const revalidate = false;
export async function GET() {
const scan = source.getPages().map(getLLMText);
const scanned = await Promise.all(scan);
return new Response(scanned.join("\n\n"));
}

View File

@@ -0,0 +1,18 @@
import { type NextRequest, NextResponse } from "next/server";
import { getLLMText } from "@/lib/get-llm-text";
import { source } from "@/lib/source";
import { notFound } from "next/navigation";
export const revalidate = false;
export async function GET(_req: NextRequest, { params }: { params: Promise<{ slug?: string[] }> }) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
return new NextResponse(await getLLMText(page));
}
export function generateStaticParams() {
return source.generateParams();
}

10
docs/app/provider.tsx Normal file
View File

@@ -0,0 +1,10 @@
"use client";
import { RootProvider } from "fumadocs-ui/provider";
import Search from "./_components/Search";
import type { ReactNode } from "react";
export function Provider({ children }: { children: ReactNode }) {
return (
<RootProvider search={{ SearchDialog: Search }}>{children}</RootProvider>
);
}

View File

@@ -0,0 +1,164 @@
"use client";
import { useMemo, useState } from "react";
import { Check, ChevronDown, Copy, ExternalLinkIcon } from "lucide-react";
import { cn } from "../../lib/cn";
import { useCopyButton } from "fumadocs-ui/utils/use-copy-button";
import { buttonVariants } from "fumadocs-ui/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "fumadocs-ui/components/ui/popover";
import { cva } from "class-variance-authority";
const cache = new Map<string, string>();
export function LLMCopyButton({
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl,
}: {
markdownUrl: string;
}) {
const [isLoading, setLoading] = useState(false);
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl);
if (cached) return navigator.clipboard.writeText(cached);
setLoading(true);
try {
await navigator.clipboard.write([
new ClipboardItem({
"text/plain": fetch(markdownUrl).then(async (res) => {
const content = await res.text();
cache.set(markdownUrl, content);
return content;
}),
}),
]);
} finally {
setLoading(false);
}
});
return (
<button
disabled={isLoading}
className={cn(
buttonVariants({
color: "secondary",
size: "sm",
className: "gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground",
}),
)}
onClick={onClick}
>
{checked ? <Check /> : <Copy />}
Copy Markdown
</button>
);
}
const optionVariants = cva(
"text-sm p-2 rounded-lg inline-flex items-center gap-2 hover:text-fd-accent-foreground hover:bg-fd-accent [&_svg]:size-4",
);
export function ViewOptions({
markdownUrl,
githubUrl,
}: {
/**
* A URL to the raw Markdown/MDX content of page
*/
markdownUrl: string;
/**
* Source file URL on GitHub
*/
githubUrl: string;
}) {
const items = useMemo(() => {
const fullMarkdownUrl =
typeof window !== "undefined" ? new URL(markdownUrl, window.location.origin) : "loading";
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
return [
/* {
title: "Open in GitHub",
href: githubUrl,
icon: (
<svg fill="currentColor" role="img" viewBox="0 0 24 24">
<title>GitHub</title>
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
),
}, */
{
title: "Open in ChatGPT",
href: `https://chatgpt.com/?${new URLSearchParams({
hints: "search",
q,
})}`,
icon: (
<svg
role="img"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<title>OpenAI</title>
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
</svg>
),
},
{
title: "Open in Claude",
href: `https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
<svg
fill="currentColor"
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>Anthropic</title>
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
</svg>
),
},
];
}, [githubUrl, markdownUrl]);
return (
<Popover>
<PopoverTrigger
className={cn(
buttonVariants({
color: "secondary",
size: "sm",
className: "gap-2",
}),
)}
>
Open
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
</PopoverTrigger>
<PopoverContent className="flex flex-col overflow-auto">
{items.map((item) => (
<a
key={item.href}
href={item.href}
rel="noreferrer noopener"
target="_blank"
className={cn(optionVariants())}
>
{item.icon}
{item.title}
<ExternalLinkIcon className="text-fd-muted-foreground size-3.5 ms-auto" />
</a>
))}
</PopoverContent>
</Popover>
);
}

View File

@@ -1,23 +0,0 @@
---
title: "Concepts"
---
There are a few important concepts around **bknd** you should be aware of in order to understand
it better.
## Primitive
Instead of going to deep into specific use-cases, **bknd** is designed to implement the lower
level functionalities in order to provide a solid foundation for building more complex logic.
## Minimal database
Although we focus on SQL-databases, we follow the priciple of moving as much logic as possible to
the application layer. E.g. default values are computed application side instead
of instruction the database to do so. This enables us to support a wide range of databases in
the future.
## Lowest runtime first
The main development target is Cloudflare Workers, since it's the most limited JavaScript
runtime built on Web Standards. If it runs there, it will run (probably) anywhere.
## Web Standards
...

View File

@@ -1,24 +1,25 @@
---
title: bknd.config.ts
tags: ["documentation"]
---
The central configuration file to extend bknd should be placed in the root of your project, so that the [CLI](/usage/cli#using-configuration-file-bknd-config) can automatically pick it up. It allows to:
* define your database connection centrally
* pass in [initial configuration](/usage/database#initial-structure) or [data seeds](/usage/database#seeding-the-database) when booting the first time
* add plugins to the app
* hook into system events
* define custom routes and endpoints
- define your database connection centrally
- pass in [initial configuration](/usage/database#initial-structure) or [data seeds](/usage/database#seeding-the-database) when booting the first time
- add plugins to the app
- hook into system events
- define custom routes and endpoints
A simple example of a `bknd.config.ts` file:
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd/adapter";
export default {
connection: {
url: "file:data.db",
}
connection: {
url: "file:data.db",
},
} satisfies BkndConfig;
```
@@ -26,57 +27,62 @@ export default {
The `BkndConfig` extends the [`CreateAppConfig`](/usage/introduction#configuration-createappconfig) type with the following properties:
{/* <AutoTypeTable path="../app/src/adapter/index.ts" name="BkndConfig" /> */}
```typescript
export type BkndConfig = CreateAppConfig & {
// return the app configuration as object or from a function
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
// called before the app is built
beforeBuild?: (app: App) => Promise<void>;
// called after the app has been built
onBuilt?: (app: App) => Promise<void>;
// passed as the first argument to the `App.build` method
buildConfig?: Parameters<App["build"]>[0];
// force the app to be recreated
force?: boolean;
// the id of the app, defaults to `app`
id?: string;
}
// return the app configuration as object or from a function
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
// called before the app is built
beforeBuild?: (app: App) => Promise<void>;
// called after the app has been built
onBuilt?: (app: App) => Promise<void>;
// passed as the first argument to the `App.build` method
buildConfig?: Parameters<App["build"]>[0];
// force the app to be recreated
force?: boolean;
// the id of the app, defaults to `app`
id?: string;
};
```
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
### `app` (CreateAppConfig)
The `app` property is a function that returns a `CreateAppConfig` object. It allows to pass in the environment variables to the configuration object.
```typescript
import type { BkndConfig } from "bknd/adapter";
export default {
app: ({ env }) => ({
connection: {
url: env.DB_URL,
}
})
app: ({ env }) => ({
connection: {
url: env.DB_URL,
},
}),
} satisfies BkndConfig;
```
See [Database](/usage/database) for more information on how to configure the database connection.
### `beforeBuild`
The `beforeBuild` property is an async function that is called before the app is built. It allows to modify the app instance that may influence the build process.
```typescript
import type { BkndConfig } from "bknd/adapter";
export default {
beforeBuild: async (app: App) => {
// do something that has to happen before the app is built
}
}
beforeBuild: async (app: App) => {
// do something that has to happen before the app is built
},
};
```
### `onBuilt`
The `onBuilt` property is an async function that is called after the app has been built. It allows to hook into the app after it has been built. This is useful for defining event listeners or register custom routes, as both the event manager and the server are recreated during the build process.
```typescript
@@ -84,21 +90,22 @@ import { type App, AppEvents } from "bknd";
import type { BkndConfig } from "bknd/adapter";
export default {
onBuilt: async (app: App) => {
console.log("App built", app.version());
// registering an event listener
app.emgr.onEvent(AppEvents.AppRequest, (event) => {
console.log("Request received", event.request.url);
})
// registering a custom route
app.server.get("/hello", (c) => c.text("Hello World"));
}
}
onBuilt: async (app: App) => {
console.log("App built", app.version());
// registering an event listener
app.emgr.onEvent(AppEvents.AppRequest, (event) => {
console.log("Request received", event.request.url);
});
// registering a custom route
app.server.get("/hello", (c) => c.text("Hello World"));
},
};
```
### `force` & `id`
The `force` property is a boolean that forces the app to be recreated. This is mainly useful for serverless environments where the execution environment is re-used, and you may or may not want to recreate the app on every request.
The `id` property is the reference in a cache map. You may create multiple instances of apps in the same process by using different ids (e.g. multi tenant applications).
@@ -113,12 +120,12 @@ Depending on which framework or runtime you're using to run bknd, the configurat
```typescript
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
// the path to the dist folder to serve static assets for the admin UI
distPath?: string;
// custom middleware to serve static assets for the admin UI
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
// options for the admin UI
adminOptions?: AdminControllerOptions | false;
// the path to the dist folder to serve static assets for the admin UI
distPath?: string;
// custom middleware to serve static assets for the admin UI
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
// options for the admin UI
adminOptions?: AdminControllerOptions | false;
};
```
@@ -129,13 +136,12 @@ export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
```typescript
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
cleanRequest?: { searchParams?: string[] };
};
```
Next.js adds the mounted path to the request object, so that the `cleanRequest` property can be used to remove the mounted path from the request URL. See other frameworks for more information on how to configure them.
## Using the configuration file
The configuration file is automatically picked up if you're using the [CLI](/usage/cli). This allows interacting with your application using the `bknd` command. For example, you can run the following command in the root of your project to start an instance:
@@ -150,4 +156,4 @@ When serving your application, you need to make sure to import the contents of y
2. create a `bknd.ts` file inside your app folder which exports helper functions to instantiate the bknd instance and retrieve the API.
3. create a catch-all route file at `src/api/[[...bknd]]/route.ts` which serves the bknd API.
This way, your application and the CLI are using the same configuration.
This way, your application and the CLI are using the same configuration.

View File

@@ -0,0 +1,141 @@
---
title: Events & Hooks
tags: ["documentation"]
---
bknd comes with a powerful built-in event system that allows you to hook into the app lifecycle and extend its functionality. You can hook into these events in two ways:
- `async`: Your listener is not blocking the main execution flow. E.g. on Cloudflare Workers, by default, the `ExecutionContext`'s `waitUntil` method is used so that the listeners runs after the response is sent.
- `sync`: Your listener is blocking the main execution flow. This allows to abort the request in your custom conditions. Some events also allow to return a modified event payload.
<Callout type="info">
Don't mistake `async` with JavaScript's `async` keyword. The `async` keyword
is used to indicate that the listener is not blocking the main execution flow.
</Callout>
## Overview
### Listening to events
You can listen to events by using the `EventManager` exposed at `app.emgr`. To register a listener, you can use either of these methods:
- `onEvent`: Register a listener for a specific event with a typed event.
- `on`: Register a listener for an event by its slug.
- `onAny`: Register a listener for all events.
```typescript
import { createApp, AppEvents } from "bknd";
const app = createApp();
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
// ^? AppRequest
console.log("Request received", event.request.url);
});
app.emgr.on("app-request", async (event) => {
console.log("Request received", event.request.url);
});
app.emgr.onAny(async (event) => {
console.log("Event received", event.slug);
});
```
You may want to register your listeners inside [`bknd.config.ts`](/extending/config) to make sure they are registered before the app is built:
```typescript title="bknd.config.ts"
import { AppEvents } from "bknd";
export default {
onBuilt: (app) => {
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
console.log("Request received", event.request.url);
});
},
};
```
### Setting a mode
By default, listeners are registered as `async` listeners, meaning that the listener is not blocking the main execution flow. You can change this by passing the mode as the third argument:
```typescript
app.emgr.onEvent(
AppEvents.AppRequest,
async (event) => {
console.log("Request received", event.request.url);
},
{ mode: "sync" },
);
```
This works for all three methods.
## App Events
These events are emitted by the `App` class and are available on the `AppEvents` object.
```typescript
import { AppEvents } from "bknd";
```
Available events:
{/* <AutoTypeTable path="../app/src/App.ts" name="AppEvents" /> */}
| Event | Params | Description |
|:------|:--------|:------------|
| `AppConfigUpdatedEvent` | `{ app: App }` | Emitted when the app configuration is updated |
| `AppBuiltEvent` | `{ app: App }` | Emitted when the app is built |
| `AppFirstBoot` | `{ app: App }` | Emitted when the app is first booted |
| `AppRequest` | `{ app: App, request: Request }` | Emitted when a request is received |
| `AppBeforeResponse` | `{ app: App, request: Request, response: Response }` | Emitted before a response is sent |
## Database Events
These events are emitted by the `Database` class and are available on the `DatabaseEvents` object. These are divided by events triggered by the `Mutator` and `Repository` classes.
```typescript
import { DatabaseEvents } from "bknd";
```
### Mutator Events
These events are emitted during database mutations (insert, update, delete operations).
| Event | Params | Description |
| :-------------------- | :-------------------------------------------------------------------------------------- | :---------------------------------------------------------- |
| `MutatorInsertBefore` | `{ entity: Entity, data: EntityData }` | Emitted before inserting a new record. Can modify the data. |
| `MutatorInsertAfter` | `{ entity: Entity, data: EntityData, changed: EntityData }` | Emitted after inserting a new record. |
| `MutatorUpdateBefore` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted before updating a record. Can modify the data. |
| `MutatorUpdateAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData, changed: EntityData }` | Emitted after updating a record. |
| `MutatorDeleteBefore` | `{ entity: Entity, entityId: PrimaryFieldType }` | Emitted before deleting a record. |
| `MutatorDeleteAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted after deleting a record. |
### Repository Events
These events are emitted during database queries (find operations).
| Event | Params | Description |
| :------------------------- | :--------------------------------------------------------- | :--------------------------------------- |
| `RepositoryFindOneBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding a single record. |
| `RepositoryFindOneAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding a single record. |
| `RepositoryFindManyBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding multiple records. |
| `RepositoryFindManyAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding multiple records. |
## Media Events
These events are emitted by the `Storage` class and are available on the `MediaEvents` object.
```typescript
import { MediaEvents } from "bknd";
```
| Event | Params | Description |
| :------------------ | :--------------------------------------- | :----------------------------------------------------------------------- |
| `FileUploadedEvent` | `{ file: FileBody } & FileUploadPayload` | Emitted when a file is successfully uploaded. Can modify the event data. |
| `FileDeletedEvent` | `{ name: string }` | Emitted when a file is deleted. |
| `FileAccessEvent` | `{ name: string }` | Emitted when a file is accessed. |
## Auth Events
Coming soon.

View File

@@ -1,69 +1,95 @@
---
title: 'Astro'
description: 'Run bknd inside Astro'
title: "Astro"
description: "Run bknd inside Astro"
tags: ["documentation"]
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
To get started with Astro 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 Astro CLI starter project by running the following command:
### CLI Starter
```sh
npx bknd create -i astro
```
</Tab>
<Tab title="Manual">
Create a new Astro project by following the [official guide](https://docs.astro.build/en/install-and-setup/), and then install bknd as a dependency:
Create a new Astro CLI starter project by running the following command:
<InstallBknd />
```sh
npx bknd create -i astro
```
<Note>The guide below assumes you're using Astro v4. We've experienced issues with Astro DB
using v5, see [this issue](https://github.com/withastro/astro/issues/12474).</Note>
### Manual
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
```bash
npx astro add react
```
Create a new Astro project by following the [official guide](https://docs.astro.build/en/install-and-setup/), and then install bknd as a dependency:
You also need to make sure to set the output to `server` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
export default defineConfig({
output: "server",
integrations: [react()]
});
```
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
<Note>
If you don't want to use React with Astro, there is also an option to serve the bknd Admin UI
statically using Astro's middleware. In case you're interested in this, feel free to reach
out in [Discord](https://discord.gg/952SFk8Tb8) or open an [issue on GitHub](https://github.com/bknd-io/bknd/issues/new).
</Note>
</Tab>
</Tabs>
<Callout type="info">
The guide below assumes you're using Astro v4. We've experienced issues with
Astro DB using v5, see [this
issue](https://github.com/withastro/astro/issues/12474).
</Callout>
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
```bash
npx astro add react
```
You also need to make sure to set the output to `server` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
output: "server", // [!code highlight]
integrations: [react()],
});
```
<Callout type="info">
If you don't want to use React with Astro, there is also an option to serve
the bknd Admin UI statically using Astro's middleware. In case you're
interested in this, feel free to reach out in
[Discord](https://discord.gg/952SFk8Tb8) or open an [issue on
GitHub](https://github.com/bknd-io/bknd/issues/new).
</Callout>
## Configuration
<Warning>
When run with Node.js, a version of 22 (LTS) or higher is required. Please verify your version by running `node -v`, and [upgrade](https://nodejs.org/en/download/) if necessary.
</Warning>
<Callout type="warning">
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running `node -v`, and
[upgrade](https://nodejs.org/en/download/) if necessary.
</Callout>
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { AstroBkndConfig } from "bknd/adapter/astro";
export default {
connection: {
url: "file:data.db"
},
connection: {
url: "file:data.db",
},
} satisfies AstroBkndConfig;
```
@@ -75,9 +101,10 @@ export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts src/bknd.ts
```ts title="src/bknd.ts"
import type { AstroGlobal } from "astro";
import { getApp as getBkndApp } from "bknd/adapter/astro";
import config from "../bknd.config";
@@ -85,37 +112,37 @@ import config from "../bknd.config";
export { config };
export async function getApp() {
return await getBkndApp(config);
return await getBkndApp(config);
}
export async function getApi(
astro: AstroGlobal,
opts?: { mode: "static" } | { mode?: "dynamic"; verify?: boolean },
astro: AstroGlobal,
opts?: { mode: "static" } | { mode?: "dynamic"; verify?: boolean },
) {
const app = await getApp();
if (opts?.mode !== "static" && opts?.verify) {
const api = app.getApi({ headers: astro.request.headers });
await api.verifyAuth();
return api;
}
const app = await getApp();
if (opts?.mode !== "static" && opts?.verify) {
const api = app.getApi({ headers: astro.request.headers });
await api.verifyAuth();
return api;
}
return app.getApi();
return app.getApi();
}
```
Create a new catch-all route at `src/pages/api/[...api].ts`.
```ts src/pages/api/[...api].ts
```ts title="src/pages/api/[...api].ts"
import { serve } from "bknd/adapter/astro";
export const prerender = false;
export const ALL = serve({
connection: {
// location of your local Astro DB
// make sure to use a remote URL in production
url: "file:.astro/content.db"
}
connection: {
// location of your local Astro DB
// make sure to use a remote URL in production
url: "file:.astro/content.db",
},
});
```
@@ -124,8 +151,10 @@ special case of astro, you may also use your Astro DB credentials since it's als
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.
## Enabling the Admin UI
Create a new catch-all route at `src/pages/admin/[...admin].astro`:
```jsx src/pages/admin/[...admin].astro
```jsx title="src/pages/admin/[...admin].astro"
---
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
@@ -154,10 +183,12 @@ export const prerender = false;
```
## Example usage of the API
You use the API in both static and SSR pages. Just note that on static pages, authentication
might not work as expected, because Cookies are not available in the static context.
Here is an example of using the API in static context:
```jsx
---
import { getApi } from "bknd/adapter/astro";
@@ -173,6 +204,7 @@ const { data } = await api.data.readMany("todos");
```
On SSR pages, you can also access the authenticated user:
```jsx
---
import { getApi } from "bknd/adapter/astro";
@@ -194,4 +226,4 @@ export const prerender = false;
```
Check the [astro repository example](https://github.com/bknd-io/bknd/tree/main/examples/astro)
for more implementation details or a [fully working example using Astro DB](https://github.com/dswbx/bknd-astro-example).
for more implementation details or a [fully working example using Astro DB](https://github.com/dswbx/bknd-astro-example).

View File

@@ -0,0 +1,3 @@
{
"pages": ["nextjs", "react-router", "astro", "vite"]
}

View File

@@ -1,42 +1,62 @@
---
title: 'Next.js'
description: 'Run bknd inside Next.js'
title: "Next.js"
description: "Run bknd inside Next.js"
tags: ["documentation"]
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
To get started with Next.js 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 Next.js CLI starter project by running the following command:
To get started with Next.js and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter.
```sh
npx bknd create -i nextjs
```
</Tab>
<Tab title="Manual">
Create a new Next.js project by following the [official guide](https://nextjs.org/docs/pages/api-reference/cli/create-next-app), and then install bknd as a dependency:
### CLI Starter
Create a new Next.js CLI starter project by running the following command:
```sh
npx bknd create -i nextjs
```
### Manual
Create a new Next.js project by following the [official guide](https://nextjs.org/docs/pages/api-reference/cli/create-next-app), and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
<InstallBknd />
</Tab>
</Tabs>
## Configuration
<Warning>
When run with Node.js, a version of 22 (LTS) or higher is required. Please verify your version by running `node -v`, and [upgrade](https://nodejs.org/en/download/) if necessary.
</Warning>
<Callout type="warning">
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running `node -v`, and
[upgrade](https://nodejs.org/en/download/) if necessary.
</Callout>
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
export default {
connection: {
url: "file:data.db"
},
connection: {
url: "file:data.db",
},
} satisfies NextjsBkndConfig;
```
@@ -45,39 +65,45 @@ See [bknd.config.ts](/extending/config) for more information on how to configure
```typescript
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
cleanRequest?: { searchParams?: string[] };
};
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts src/bknd.ts
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
```ts title="src/bknd.ts"
import {
type NextjsBkndConfig,
getApp as getBkndApp,
} from "bknd/adapter/nextjs";
import { headers } from "next/headers";
import config from "../bknd.config";
export { config };
export async function getApp() {
return await getBkndApp(config, process.env);
return await getBkndApp(config, process.env);
}
export async function getApi(opts?: { verify?: boolean }) {
const app = await getApp();
if (opts?.verify) {
const api = app.getApi({ headers: await headers() });
await api.verifyAuth();
return api;
}
const app = await getApp();
if (opts?.verify) {
const api = app.getApi({ headers: await headers() });
await api.verifyAuth();
return api;
}
return app.getApi();
return app.getApi();
}
```
For more information about the connection object, refer to the [Database](/usage/database) guide.
Now to expose the API, create a catch-all route file at `src/api/[[...bknd]]/route.ts`:
```ts src/api/[[...bknd]]/route.ts
```ts title="src/api/[[...bknd]]/route.ts"
import { config } from "@/bknd";
import { serve } from "bknd/adapter/nextjs";
@@ -85,12 +111,12 @@ import { serve } from "bknd/adapter/nextjs";
export const runtime = "edge";
const handler = serve({
...config,
cleanRequest: {
// depending on what name you used for the catch-all route,
// you need to change this to clean it from the request.
searchParams: ["bknd"],
},
...config,
cleanRequest: {
// depending on what name you used for the catch-all route,
// you need to change this to clean it from the request.
searchParams: ["bknd"],
},
});
export const GET = handler;
@@ -101,43 +127,48 @@ export const DELETE = handler;
```
## Enabling the Admin UI
Create a page at `admin/[[...admin]]/page.tsx`:
```tsx admin/[[...admin]]/page.tsx
```tsx title="admin/[[...admin]]/page.tsx"
import { Admin } from "bknd/ui";
import { getApi } from "@/bknd";
import "bknd/dist/styles.css";
export default async function AdminPage() {
// make sure to verify auth using headers
const api = await getApi({ verify: true });
// make sure to verify auth using headers
const api = await getApi({ verify: true });
return (
<Admin
withProvider={{ user: api.getUser() }}
config={{
basepath: "/admin",
logo_return_path: "/../",
color_scheme: "system",
}}
/>
);
return (
<Admin
withProvider={{ user: api.getUser() }}
config={{
basepath: "/admin",
logo_return_path: "/../",
color_scheme: "system",
}}
/>
);
}
```
## Example usage of the API
You can use the `getApi` helper function we've already set up to fetch and mutate in static pages and server components:
```tsx app/page.tsx
```tsx title="app/page.tsx"
import { getApi } from "@/bknd";
export default async function Home() {
const api = await getApi();
const { data: todos } = await api.data.readMany("todos", { limit: 5 });
const api = await getApi();
const { data: todos } = await api.data.readMany("todos", { limit: 5 });
return <ul>
return (
<ul>
{todos.map((todo) => (
<li key={String(todo.id)}>{todo.title}</li>
<li key={String(todo.id)}>{todo.title}</li>
))}
</ul>
</ul>
);
}
```
```

View File

@@ -0,0 +1,195 @@
---
title: "React Router"
description: "Run bknd inside React Router"
tags: ["documentation"]
---
## Installation
To get started with React Router and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
### CLI Starter
Create a new React Router CLI starter project by running the following command:
```sh
npx bknd create -i react-router
```
### Manual
Create a new React Router project by following the [official guide](https://reactrouter.com/start/framework/installation), and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
</Tabs>
## Configuration
<Callout type="warning">
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running `node -v`, and
[upgrade](https://nodejs.org/en/download/) if necessary.
</Callout>
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript title="bknd.config.ts"
import type { ReactRouterBkndConfig } from "bknd/adapter/react-router";
export default {
connection: {
url: "file:data.db",
},
} satisfies ReactRouterBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `ReactRouterBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type ReactRouterEnv = NodeJS.ProcessEnv;
type ReactRouterFunctionArgs = {
request: Request;
};
export type ReactRouterBkndConfig<Env = ReactRouterEnv> =
FrameworkBkndConfig<Env>;
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts title="app/bknd.ts"
import {
type ReactRouterBkndConfig,
getApp as getBkndApp,
} from "bknd/adapter/react-router";
import config from "../bknd.config";
export { config };
// you may adjust this function depending on your runtime environment.
// e.g. when deploying to cloudflare workers, you'd want the FunctionArgs to be passed in
// to resolve environment variables
export async function getApp() {
return await getBkndApp(config, process.env as any);
}
export async function getApi(
args?: { request: Request },
opts?: { verify?: boolean },
) {
const app = await getApp();
if (opts?.verify) {
const api = app.getApi({ headers: args?.request.headers });
await api.verifyAuth();
return api;
}
return app.getApi();
}
```
For more information about the connection object, refer to the [Database](/usage/database) guide.
Create a new api splat route file at `app/routes/api.$.ts`:
```ts title="app/routes/api.$.ts"
import { getApp } from "~/bknd";
const handler = async (args: { request: Request }) => {
const app = await getApp();
return app.fetch(args.request);
};
export const loader = handler;
export const action = handler;
```
## Enabling the Admin UI
Create a new splat route file at `app/routes/admin.$.tsx`:
```tsx title="app/routes/admin.$.tsx"
import { lazy, Suspense, useSyncExternalStore } from "react";
import { type LoaderFunctionArgs, useLoaderData } from "react-router";
import { getApi } from "~/bknd";
const Admin = lazy(() =>
import("bknd/ui").then((mod) => ({ default: mod.Admin })),
);
import "bknd/dist/styles.css";
export const loader = async (args: LoaderFunctionArgs) => {
const api = await getApi(args, { verify: true });
return {
user: api.getUser(),
};
};
export default function AdminPage() {
const { user } = useLoaderData<typeof loader>();
// derived from https://github.com/sergiodxa/remix-utils
// @ts-ignore
const hydrated = useSyncExternalStore(
() => {},
() => true,
() => false,
);
if (!hydrated) return null;
return (
<Suspense>
<Admin
withProvider={{ user }}
config={{ basepath: "/admin", logo_return_path: "/../" }}
/>
</Suspense>
);
}
```
## Example usage of the API
You can use the `getApi` helper function we've already set up to fetch and mutate:
```tsx title="app/routes/_index.tsx"
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
import { getApi } from "~/bknd";
export const loader = async (args: LoaderFunctionArgs) => {
// use authentication from request
const api = await getApi(args, { verify: true });
const { data } = await api.data.readMany("todos");
return { data, user: api.getUser() };
};
export default function Index() {
const { data, user } = useLoaderData<typeof loader>();
return (
<div>
<h1>Data</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h1>User</h1>
<pre>{JSON.stringify(user, null, 2)}</pre>
</div>
);
}
```

View File

@@ -1,36 +1,59 @@
---
title: 'Vite'
description: 'Run bknd inside Vite'
title: "Vite"
description: "Run bknd inside Vite"
tags: ["documentation"]
---
import InstallBknd from '/snippets/install-bknd.mdx';
Vite is a powerful toolkit to accelerate your local development.
## Installation
Create a new vite project by following the [official guide](https://vite.dev/guide/#scaffolding-your-first-vite-project)
and then install bknd as a dependency:
<InstallBknd />
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
</Tabs>
Additionally, install required dependencies:
```bash
npm install @hono/vite-dev-server
```
## Configuration
<Warning>
When run with Node.js, a version of 22 (LTS) or higher is required. Please verify your version by running `node -v`, and [upgrade](https://nodejs.org/en/download/) if necessary.
</Warning>
<Callout type="warning">
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running `node -v`, and
[upgrade](https://nodejs.org/en/download/) if necessary.
</Callout>
Now create a `bknd.config.ts` file in the root of your project.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { ViteBkndConfig } from "bknd/adapter/vite";
export default {
connection: {
url: "file:data.db"
}
connection: {
url: "file:data.db",
},
} satisfies ViteBkndConfig;
```
@@ -43,22 +66,25 @@ export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {};
```
## Serve the API
To serve the **bknd** API, you first have to create a local server file for you vite environment.
Create a `server.ts` file:
```typescript
```typescript title="server.ts"
import { serve } from "bknd/adapter/vite";
import config from "./bknd.config";
export default serve(config)
export default serve(config);
```
You can also run your vite server in `mode: "fresh"`, this will re-create the app on every fetch.
This is only useful for when working on the `bknd` repository directly.
For more information about the connection object, refer to the [Database](/usage/database) guide.
Next, adjust your `vite.config.ts` to look like the following:
```ts
```ts title="vite.config.ts"
import { devServer } from "bknd/adapter/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
@@ -67,13 +93,13 @@ import tsconfigPaths from "vite-tsconfig-paths";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
tsconfigPaths(),
devServer({
// point to your previously created server file
entry: "./server.ts"
})
]
react(),
tsconfigPaths(),
devServer({
// point to your previously created server file
entry: "./server.ts",
}),
],
});
```
@@ -82,64 +108,68 @@ looks like an empty project. That's because we only registered the API, head ove
http://localhost:5174/api/system/config to see **bknd** respond.
## Serve the Admin UI
After adding the API, you can easily add the Admin UI by simply returning it in your `App.tsx`.
Replace all of its content with the following:
```tsx
```tsx title="App.tsx"
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
export default function App() {
return <Admin withProvider />
return <Admin withProvider />;
}
```
Now http://localhost:5174/ should give you the Admin UI.
## Customizations
This is just the bare minimum and may not always fulfill your requirements. There are a few
options you can make use of to adjust it according to your setup.
### Use custom HTML to serve the Admin UI
There might be cases you want to be sure to be in control over the HTML that is being used.
`bknd` generates it automatically, but you use your own one as follows:
```typescript server.ts
```typescript title="server.ts"
import { serve, addViteScript } from "bknd/adapter/vite";
import { readFile } from "node:fs/promises"
import { readFile } from "node:fs/promises";
import config from "./bknd.config";
let html = await readFile("./index.html", "utf-8");
// then add it as an option
export default serve({
...config,
adminOptions: {
html: addViteScript(html),
// optionally, you can change the base path for the admin UI
adminBasePath: "/admin"
}
})
export default serve({
...config,
adminOptions: {
html: addViteScript(html),
// optionally, you can change the base path for the admin UI
adminBasePath: "/admin",
},
});
```
The vite scripts has to be added manually currently, as adding them automatically with
`@hono/vite-dev-server` is buggy. This may change in the future.
### Use a custom entry point
By default, the entry point `/src/main.tsx` is used and should fit most cases. If that's not you,
you can supply a different one like so:
```typescript server.ts
```typescript title="server.ts"
import { serve } from "bknd/adapter/vite";
import config from "./bknd.config";
// the configuration given is optional
export default serve({
...config,
adminOptions: {
forceDev: {
mainPath: "/src/special.tsx"
}
}
...config,
adminOptions: {
forceDev: {
mainPath: "/src/special.tsx",
},
},
});
```
```

View File

@@ -1,44 +1,65 @@
---
title: 'AWS Lambda'
description: 'Run bknd inside AWS Lambda'
title: "AWS Lambda"
description: "Run bknd inside AWS Lambda"
tags: ["documentation"]
---
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:
### CLI Starter
```sh
npx bknd create -i aws
```
</Tab>
<Tab title="Manual">
Create a new AWS Lambda project and then install bknd as a dependency:
Create a new Bun CLI starter project by running the following command:
```sh
npx bknd create -i aws
```
### Manual
Create a new AWS Lambda project and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
<InstallBknd />
</Tab>
</Tabs>
## Serve the API
To serve the API, you can use the `serveLambda` function of the AWS Lambda adapter.
```tsx index.mjs
```tsx title="index.mjs"
import { serveLambda } from "bknd/adapter/aws";
import { libsql } from "bknd/data";
import { libsql } from "bknd";
export const handler = serveLambda({
connection: libsql({
url: "libsql://your-database-url.turso.io",
authToken: "your-auth-token",
}),
connection: libsql({
url: "libsql://your-database-url.turso.io",
authToken: "your-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. Instead, we recommend you to use [LibSQL on Turso](/usage/database#sqlite-using-libsql-on-turso).
## 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:
@@ -49,35 +70,37 @@ 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}
```tsx title="index.mjs"
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"
}
connection: {
url: process.env.DB_URL!,
authToken: process.env.DB_AUTH_TOKEN!,
},
assets: {
// [!code highlight]
mode: "local", // [!code highlight]
root: "./static", // [!code highlight]
}, // [!code highlight]
});
```
## 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
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
@@ -86,7 +109,7 @@ 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
```sh title="deploy.sh"
# cat deploy.sh | head -12
FUNCTION_NAME="bknd-lambda"
ROLE_NAME="bknd-lambda-execution-role"
@@ -105,4 +128,3 @@ To clean up AWS resources created by the deployment script, you can run:
```bash
npm run clean
```

View File

@@ -1,46 +1,65 @@
---
title: 'Bun'
description: 'Run bknd inside Bun'
title: "Bun"
description: "Run bknd inside Bun"
tags: ["documentation"]
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
To get started with Bun 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:
## CLI Starter
```sh
npx bknd create -i bun
```
</Tab>
<Tab title="Manual">
Create a new Bun project and then install bknd as a dependency:
Create a new Bun CLI starter project by running the following command:
```sh
npx bknd create -i bun
```
### Manual
Create a new Bun project and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
<InstallBknd />
</Tab>
</Tabs>
## Serve the API & static files
The `serve` function of the Bun adapter makes sure to also serve the static files required for
the admin panel.
``` tsx
// index.ts
```tsx title="index.ts"
import { serve } from "bknd/adapter/bun";
// if the configuration is omitted, it uses an in-memory database
serve({
connection: {
url: "file:data.db"
}
connection: {
url: "file:data.db",
},
});
```
For more information about the connection object, refer to the [Database](/usage/database) guide.
Run the application using Bun by executing:
```bash
bun run index.ts
```
```

View File

@@ -1,33 +1,51 @@
---
title: 'Cloudflare'
description: 'Run bknd inside Cloudflare Worker'
title: "Cloudflare"
description: "Run bknd inside Cloudflare Worker"
tags: ["documentation"]
---
import InstallBknd from '/snippets/install-bknd.mdx';
## Installation
To get started with Cloudflare Workers 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 Cloudflare CLI starter project by running the following command:
### CLI Starter
```sh
npx bknd create -i cloudflare
```
</Tab>
<Tab title="Manual">
Create a new cloudflare worker project by following the [official guide](https://developers.cloudflare.com/workers/get-started/guide/), and then install bknd as a dependency:
Create a new Cloudflare CLI starter project by running the following command:
```sh
npx bknd create -i cloudflare
```
### Manual
Create a new cloudflare worker project by following the [official guide](https://developers.cloudflare.com/workers/get-started/guide/), and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
<InstallBknd />
</Tab>
</Tabs>
## Serve the API
If you don't choose anything specific, the following code will use the `warm` mode and uses the first D1 binding it finds. See the
chapter [Using a different mode](#using-a-different-mode) for available modes.
```ts src/index.ts
```ts title="src/index.ts"
import { serve, d1 } from "bknd/adapter/cloudflare";
// scans your environment for the first D1 binding it finds
@@ -35,23 +53,24 @@ export default serve();
// manually specifying a D1 binding:
export default serve<Env>({
app: ({ env }) => d1({ binding: env.D1_BINDING })
app: ({ env }) => d1({ binding: env.D1_BINDING }),
});
// or specify binding using `bindings`
export default serve<Env>({
bindings: ({ env }) => ({ db: env.D1_BINDING })
bindings: ({ env }) => ({ db: env.D1_BINDING }),
});
// or use LibSQL
export default serve<Env>({
app: ({ env }) => ({ url: env.DB_URL })
app: ({ env }) => ({ url: env.DB_URL }),
});
```
For more information about the connection object when using LibSQL, refer to the [Database](/usage/database) guide.
Now run the worker:
```bash
wrangler dev
```
@@ -60,76 +79,86 @@ And confirm it works by opening [http://localhost:8787](http://localhost:8787) i
your browser.
## Serve the Admin UI
Now in order to also server the static admin files, you have to modify the `wrangler.toml` to include the static assets. You can do so by either serving the static using the new [Assets feature](https://developers.cloudflare.com/workers/static-assets/), or the deprecated [Workers Site](https://developers.cloudflare.com/workers/configuration/sites/configuration/).
<Tabs>
<Tab title="Assets">
Make sure your assets point to the static assets included in the bknd package:
### Assets
```toml wrangler.toml
assets = { directory = "node_modules/bknd/dist/static" }
```
Make sure your assets point to the static assets included in the bknd package:
</Tab>
<Tab title="Workers Sites">
Make sure your site points to the static assets included in the bknd package:
```toml title="wrangler.toml"
assets = { directory = "node_modules/bknd/dist/static" }
```
```toml wrangler.toml
[site]
bucket = "node_modules/bknd/dist/static"
```
### Workers Sites
And then modify the worker entry as follows:
```ts {2, 6} src/index.ts
import { serve } from "bknd/adapter/cloudflare";
import manifest from "__STATIC_CONTENT_MANIFEST";
Make sure your site points to the static assets included in the bknd package:
export default serve<Env>({
app: () => ({/* ... */}),
manifest
});
```
</Tab>
</Tabs>
```toml title="wrangler.toml"
[site]
bucket = "node_modules/bknd/dist/static"
```
And then modify the worker entry as follows:
```ts title="src/index.ts"
import { serve } from "bknd/adapter/cloudflare";
import manifest from "__STATIC_CONTENT_MANIFEST"; // [!code highlight]
export default serve<Env>({
app: () => ({
/* ... */
}),
manifest, // [!code highlight]
});
```
## Adding custom routes
You can also add custom routes by defining them after the app has been built, like so:
```ts {5-7}
```ts
import { serve } from "bknd/adapter/cloudflare";
export default serve<Env>({
// ...
onBuilt: async (app) => {
app.server.get("/hello", (c) => c.json({ hello: "world" }));
}
// ...
onBuilt: async (app) => {
// [!code highlight]
app.server.get("/hello", (c) => c.json({ hello: "world" })); // [!code highlight]
}, // [!code highlight]
});
```
The property `app.server` is a [Hono](https://hono.dev/) instance, you can literally anything you can do with Hono.
## Using a different mode
With the Cloudflare Workers adapter, you're being offered to 4 modes to choose from (default:
`warm`):
| Mode | Description | Use Case |
|:----------|:-------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------|
| :-------- | :----------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------- |
| `fresh` | On every request, the configuration gets refetched, app built and then served. | Ideal if you don't want to deal with eviction, KV or Durable Objects. |
| `warm` | It tries to keep the built app in memory for as long as possible, and rebuilds if evicted. | Better response times, should be the default choice. |
| `cache` | The configuration is fetched from KV to reduce the initial roundtrip to the database. | Generally faster response times with irregular access patterns. |
| `durable` | The bknd app is ran inside a Durable Object and can be configured to stay alive. | Slowest boot time, but fastest responses. Can be kept alive for as long as you want, giving similar response times as server instances. |
### Modes: `fresh` and `warm`
To use either `fresh` or `warm`, all you have to do is adding the desired mode to `cloudflare.
mode`, like so:
```ts
import { serve } from "bknd/adapter/cloudflare";
export default serve({
// ...
mode: "fresh" // mode: "fresh" | "warm" | "cache" | "durable"
// ...
mode: "fresh", // mode: "fresh" | "warm" | "cache" | "durable"
});
```
### Mode: `cache`
For the cache mode to work, you also need to specify the KV to be used. For this, use the
`bindings` property:
@@ -137,29 +166,32 @@ For the cache mode to work, you also need to specify the KV to be used. For this
import { serve } from "bknd/adapter/cloudflare";
export default serve<Env>({
// ...
mode: "cache",
bindings: ({ env }) => ({ kv: env.KV })
// ...
mode: "cache",
bindings: ({ env }) => ({ kv: env.KV }),
});
```
### Mode: `durable` (advanced)
To use the `durable` mode, you have to specify the Durable Object to extract from your
environment, and additionally export the `DurableBkndApp` class:
```ts
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
export { DurableBkndApp };
export default serve<Env>({
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60 // optional
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60, // optional
});
```
Next, you need to define the Durable Object in your `wrangler.toml` file (refer to the [Durable
Objects](https://developers.cloudflare.com/durable-objects/) documentation):
```toml
[[durable_objects.bindings]]
name = "DOBJ"
@@ -173,25 +205,28 @@ new_classes = ["DurableBkndApp"]
Since the communication between the Worker and Durable Object is serialized, the `onBuilt`
property won't work. To use it (e.g. to specify special routes), you need to extend from the
`DurableBkndApp`:
```ts
import type { App } from "bknd";
import { serve, DurableBkndApp } from "bknd/adapter/cloudflare";
export default serve({
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60 // optional
// ...
mode: "durable",
bindings: ({ env }) => ({ dobj: env.DOBJ }),
keepAliveSeconds: 60, // optional
});
export class CustomDurableBkndApp extends DurableBkndApp {
async onBuilt(app: App) {
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
}
async onBuilt(app: App) {
app.modules.server.get("/custom/endpoint", (c) => c.text("Custom"));
}
}
```
In case you've already deployed your Worker, the deploy command may complain about a new class
being used. To fix this issue, you need to add a "rename migration":
```toml
[[durable_objects.bindings]]
name = "DOBJ"
@@ -208,26 +243,27 @@ deleted_classes = ["DurableBkndApp"]
```
## D1 Sessions (experimental)
D1 now supports to enable [global read replication](https://developers.cloudflare.com/d1/best-practices/read-replication/). This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the `d1.session` property:
```typescript src/index.ts
```typescript title="src/index.ts"
import { serve } from "bknd/adapter/cloudflare";
export default serve({
// currently recommended to use "fresh" mode
// otherwise consecutive requests will use the same bookmark
mode: "fresh",
// ...
d1: {
// enables D1 sessions
session: true,
// (optional) restrict the transport, options: "header" | "cookie"
// if not specified, it supports both
transport: "cookie",
// (optional) choose session constraint if not bookmark present
// options: "first-primary" | "first-unconstrained"
first: "first-primary"
}
// currently recommended to use "fresh" mode
// otherwise consecutive requests will use the same bookmark
mode: "fresh",
// ...
d1: {
// enables D1 sessions
session: true,
// (optional) restrict the transport, options: "header" | "cookie"
// if not specified, it supports both
transport: "cookie",
// (optional) choose session constraint if not bookmark present
// options: "first-primary" | "first-unconstrained"
first: "first-primary",
},
});
```
@@ -235,4 +271,4 @@ If bknd is used in a stateful user context (like in a browser), it'll automatica
```bash
curl -H "x-cf-d1-session: <bookmark>" ...
```
```

View File

@@ -1,14 +1,17 @@
---
title: 'Docker'
description: 'Official docker image for bknd'
title: "Docker"
description: "Official docker image for bknd"
tags: ["documentation"]
---
# Official `bknd` Docker image
The docker image intentially doesn't copy any data into the image for now, so you can copy the Dockerfile and build the image anywhere.
Locate the Dockerfile either by pulling the [repository](https://github.com/bknd-io/bknd) and navigating to the `docker` directory, or download from [here](https://github.com/bknd-io/bknd/blob/main/docker/Dockerfile).
## Building the Docker image
To build the Docker image, run the following command:
```bash
@@ -16,11 +19,13 @@ docker build -t bknd .
```
If you want to override the bknd version used, you can pass a `VERSION` build argument:
```bash
docker build --build-arg VERSION=<version> -t bknd .
```
## Running the Docker container
To run the Docker container, run the following command:
```bash
@@ -34,6 +39,7 @@ docker run -p 1337:1337 -e ARGS="--db-url file:/data/data.db" bknd
```
To mount the data directory to the host, you can use the `-v` flag:
```bash
docker run -p 1337:1337 -v /path/to/data:/data bknd
```
@@ -42,7 +48,7 @@ docker run -p 1337:1337 -v /path/to/data:/data bknd
If you want to use docker compose and build the image directly from the git repository.
```yaml compose.yml
```yaml title="compose.yml"
services:
bknd:
pull_policy: build
@@ -55,12 +61,11 @@ services:
- ${DATA_DIR:-.}/data:/data
```
The docker compose file can be extended to build a specific version of bknd.
Extend the `build` section with `args` and `labels`.
Inside `args`, you can pass a `VERSION` build argument, and use `labels` so the built image receives a unique identifier.
```yaml compose.yml
```yaml title="compose.yml"
services:
bknd:
pull_policy: build

View File

@@ -0,0 +1,3 @@
{
"pages": ["node", "bun", "cloudflare", "aws", "docker"]
}

View File

@@ -0,0 +1,68 @@
---
title: "Node"
description: "Run bknd inside Node"
tags: ["documentation"]
---
## Installation
To get started with Node and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:
### CLI Starter
Create a new Node CLI starter project by running the following command:
```sh
npx bknd create -i node
```
### Manual
Create a new Node project and then install bknd as a dependency:
<Tabs groupId='package-manager' persist items={[ 'npm', 'pnpm', 'yarn', 'bun']}>
```bash tab="npm"
npm install bknd
```
```bash tab="pnpm"
pnpm install bknd
```
```bash tab="yarn"
yarn add bknd
```
```bash tab="bun"
bun add bknd
```
</Tabs>
## Serve the API & static files
The `serve` function of the Node adapter makes sure to also serve the static files required for
the admin panel.
```tsx title="server.ts"
import { serve } from "bknd/adapter/node";
// if the configuration is omitted, it uses an in-memory database
/** @type {import("bknd/adapter/node").NodeAdapterOptions} */
const config = {
connection: {
url: "file:data.db",
},
};
serve(config);
```
For more information about the connection object, refer to the [Database](/usage/database) guide.
Run the application using node by executing:
```bash
node server.js
```

View File

@@ -0,0 +1,116 @@
---
title: "Introduction"
description: "Integrate bknd into your runtime/framework of choice"
tags: ["documentation"]
---
import { Icon } from "@iconify/react";
## 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.
<Cards>
<Card icon={<Icon icon="tabler:brand-nextjs" className="text-fd-primary !size-6" />} title="NextJS" href="/integration/nextjs" />
<Card
icon={
<Icon icon="simple-icons:reactrouter" className="text-fd-primary !size-6" />
}
title="React Router"
href="/integration/react-router"
/>
<Card
icon={<Icon icon="simple-icons:astro" className="text-fd-primary !size-6" />}
title="Astro"
href="/integration/astro"
/>
<Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new">
Create a new issue to request a guide for your framework.
</Card>
</Cards>
## Start with a Runtime
If you prefer to use a runtime instead of a framework, you can choose from the following options. These runtimes allow you to serve the API and UI in the runtime's native server and serve the UI assets statically from `node_modules`.
<Cards>
<Card
icon={<Icon icon="tabler:brand-nodejs" className="text-fd-primary !size-6" />}
title="NodeJS"
href="/integration/node"
/>
<Card
icon={<Icon icon="simple-icons:bun" className="text-fd-primary !size-6" />}
title="Bun"
href="/integration/bun"
/>
<Card
icon={
<Icon
icon="devicon-plain:cloudflareworkers"
className="text-fd-primary !size-6"
/>
}
title="Cloudflare"
href="/integration/cloudflare"
/>
<Card
icon={<Icon icon="tabler:lambda" className="text-fd-primary !size-6" />}
title="AWS Lambda"
href="/integration/aws"
/>
<Card
icon={<Icon icon="simple-icons:vitest" className="text-fd-primary !size-6" />}
title="Vite"
href="/integration/vite"
/>
<Card
icon={
<Icon
icon="streamline-logos:docker-logo-solid"
className="text-fd-primary !size-6"
/>
}
title="Docker"
href="/integration/docker"
/>
<Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new">
Create a new issue to request a guide for your runtime.
</Card>
</Cards>
## Overview
### Serving the backend (API)
Serve the backend as an API for any JS runtime or framework. The latter is especially handy, as it allows you to deploy your frontend and backend bundled together. Furthermore it allows adding additional logic in a way you're already familar with. Just add another route and you're good to go.
Here is an example of serving the API using node:
```js title="index.js"
import { serve } from "bknd/adapter/node";
serve();
```
### Serving the Admin UI
The admin UI allows to manage your data including full configuration of your backend using a graphical user interface. Using `vite`, your admin route looks like this:
```tsx title="admin.tsx"
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
export default function AdminPage() {
return <Admin withProvider />;
}
```

View File

@@ -0,0 +1,33 @@
{
"title": "Documentation",
"root": true,
"description": "Technical reference and integration",
"icon": "Book",
"slug": "",
"pages": [
"---Getting Started---",
"start",
"motivation",
"---Usage---",
"./usage/introduction",
"./usage/database",
"./usage/cli",
"./usage/sdk",
"./usage/react",
"./usage/elements",
"---Extending---",
"./extending/config",
"./extending/events",
"---Integration---",
"./integration/introduction",
"./integration/(frameworks)/",
"./integration/(runtimes)/",
"---Modules---",
"./modules/overview",
"./modules/server",
"./modules/data",
"./modules/auth",
"./modules/media",
"./modules/flows"
]
}

View File

@@ -1,19 +1,22 @@
---
title: 'Auth'
description: 'Easily implement reliable authentication strategies.'
title: "Auth"
description: "Easily implement reliable authentication strategies."
tags: ["documentation"]
---
Authentication is essential for securing applications, and **bknd** provides a straightforward approach to implementing robust strategies.
### **Core Features**
- Automatically creates a `user` entity, with support for customizable fields.
- Authenticates users based on configurable strategies.
- Generates JWTs according to specified configurations.
- Provides session management for maintaining user authentication state.
### **Supported Authentication Strategies**
- **Email/Password**: Supports plain and SHA-256 password hashing (bcrypt planned for future
releases).
releases).
- **OAuth/OIDC**: Works with providers like Google and GitHub.
- Compatible with any specification-compliant provider.

View File

@@ -1,37 +1,47 @@
---
title: 'Data'
description: 'Define, query, and control your data with ease.'
title: "Data"
description: "Define, query, and control your data with ease."
tags: ["documentation"]
---
Data is the lifeblood of any application, and **bknd** provides the tools to handle it effortlessly. From defining entities to executing complex queries, bknd offers a developer-friendly, intuitive, and powerful data management experience.
### **Define Your Data**
Model entities with **fields** and **relationships**, automatically synced to your database.
- Supported field types: `primary`, `text`, `number`, `date`, `boolean`, `enum`, `json`, and `jsonschema`.
- Relationship types: `many-to-one`, `one-to-one`, `many-to-many`, and `polymorphic`.
- Add **indices** to enhance database performance: single, compound, and unique indices.
### **Entity Management**
Use the **EntityManager** to simplify handling your entities. Access data efficiently with the **Repository**:
- Select specific properties to return.
- Define sort directions, limits, and offsets.
- Include or join relational data seamlessly within a single query using `with` and joins.
- Apply robust filtering with an extensive `where` object: `$eq`, `$ne`, `$isnull`, `$notnull`, `$in`, `$notin`, `$gt`, `$gte`, `$lt`, `$lte`, `$between`.
### **Seamless Data Manipulation with Mutators**
Create, update, and delete entity data with confidence.
- Validates data against the entity schema.
- (coming soon) Supports relational data with flexible syntax: `$create`, `$set`, `$attach`,
`$detach`.
`$detach`.
### **Powerful Event System**
Hook into critical lifecycle events for fine-grained control:
- Repository events: `find-one-before`, `find-one-after`, `find-many-before`, `find-many-after`.
- Mutator events: `insert-before`, `insert-after`, `update-before`, `update-after`, `delete-before`, `delete-after`.
### **Enhanced Database Communication**
The **Connection** class communicates with the database. It's based on kysely, so that it
generally supports multiple database dialects (but currently only SQLite/LibSQL is supported).
Whether you're modeling simple data structures or managing complex relationships, bknd's data tools empower you to build applications with confidence and scalability.
Whether you're modeling simple data structures or managing complex relationships, bknd's data tools empower you to build applications with confidence and scalability.

View File

@@ -0,0 +1,40 @@
---
title: "Flows"
description: "Design and run workflows with seamless automation."
tags: ["documentation"]
---
<Callout type="info">
Flows is the youngest module and its UI is still in development. For the sake
of tech preview, we decided to exclude it from the Admin UI for now. It will
be available very soon.
</Callout>
**bknd** enables you to automate tasks and processes through flexible and powerful workflows.
## **Core Features**
### Trigger-Based Executions
- **Manual**: Trigger workflows manually.
- **System Events**: Automate workflows based on system events, such as:
- **Data Events**: Entity create, update, or delete.
- **Auth Events**: User sign-in, sign-up, sign-out, or deletion.
- **Media Events**: File uploads or downloads.
- **Server Events**: Before and after request/response handling.
- **HTTP Trigger**: Map workflows to any URL path for external triggers.
- **Custom Event Hooks**: Define and trigger custom events within workflows for tailored automation.
### Task Management
- Define tasks to run in sequence, parallel, or loops.
- Enable conditional task execution based on output results.
### Execution Modes
- **Asynchronous**: Run workflows in the background.
- **Synchronous**: Block further execution until the workflow completes, similar to traditional code execution.
- **Reusable Components**: Create reusable task sequences (sub-workflows) for better organization and efficiency.
- **OpenAPI Specification (OAS) Tasks**: Upload an OpenAPI spec to execute API requests as part of a workflow.

View File

@@ -1,16 +1,18 @@
---
title: 'Media'
description: 'Effortlessly manage and serve all your media files.'
title: "Media"
description: "Effortlessly manage and serve all your media files."
tags: ["documentation"]
---
**bknd** provides a flexible and efficient way to handle media files, making it easy to upload, manage, and deliver content across various platforms.
### **Core Features**
- **File Uploads**: Supports direct (chunked coming soon) uploads of files up to 5GB (depending
on your environment).
on your environment).
- **Adapter-Based Design**:
- Pre-built support for S3, S3-compatible services (e.g., R2, Tigris), and Cloudinary.
- Extend functionality by implementing custom adapters.
- Pre-built support for S3, S3-compatible services (e.g., R2, Tigris), and Cloudinary.
- Extend functionality by implementing custom adapters.
- **Entity Integration**: Attach media files directly to entities for seamless data association.
- **Content Delivery**: Serve media over an API endpoint or directly from your provider.
@@ -19,7 +21,7 @@ and control without added complexity.
### Pre-built support for S3
There are two ways to enable S3 or S3 compatible storage in bknd.
There are two ways to enable S3 or S3 compatible storage in bknd.
1. Enable via the admin UI.
@@ -28,6 +30,7 @@ There are two ways to enable S3 or S3 compatible storage in bknd.
This will automatically configure the S3 adapter for you.
**URL Format Examples:**
- **S3**: `https://{bucket}.s3.{region}.amazonaws.com`
- **R2 (Cloudflare)**: `https://{account_id}.r2.cloudflarestorage.com/{bucket}`
@@ -35,23 +38,23 @@ There are two ways to enable S3 or S3 compatible storage in bknd.
To enable using a code-first approach, create an [Initial Structure](/usage/database#initial-structure) using the `initialConfig` option in your `bknd.config.ts` file.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd/adapter";
export default {
initialConfig: {
media: {
enabled: true,
adapter: {
type: "s3",
config: {
access_key: "<key>",
secret_access_key: "<secret key>",
url: "<bucket url>"
}
}
}
}
initialConfig: {
media: {
enabled: true,
adapter: {
type: "s3",
config: {
access_key: "<key>",
secret_access_key: "<secret key>",
url: "<bucket url>",
},
},
},
},
} satisfies BkndConfig;
```
@@ -59,7 +62,7 @@ export default {
For local development and testing, you can use the local file system adapter. This is particularly useful when working with Node.js environments.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import { registerLocalMediaAdapter } from "bknd/adapter/node";
import type { BkndConfig } from "bknd/adapter";
@@ -67,17 +70,17 @@ import type { BkndConfig } from "bknd/adapter";
const local = registerLocalMediaAdapter();
export default {
initialConfig: {
media: {
enabled: true,
adapter: local({
path: "./public/uploads", // Files will be stored in this directory
}),
}
}
initialConfig: {
media: {
enabled: true,
adapter: local({
path: "./public/uploads", // Files will be stored in this directory
}),
},
},
} satisfies BkndConfig;
```
This configuration will store uploaded files in the specified directory,
This configuration will store uploaded files in the specified directory,
making them accessible through your application's public path (in this case)
or at `/api/media/file/{filename}`.

View File

@@ -0,0 +1,53 @@
---
title: "Overview"
description: "General overview of the system"
tags: ["documentation"]
---
import { Icon } from "@iconify/react";
This backend system focuses on 4 essential building blocks which can be tightly connected:
Data, Auth, Media and Flows.
The main idea is to supply all baseline functionality required in order to accomplish complex
logic and behaviors instead of hard coding it into the system code. This way, almost any part
of the application is customizable and additionally won't limit you to what it can do.
<Cards>
<Card icon={<Icon icon="majesticons:data-line" className="text-fd-primary !size-6" />} title="Data" href="/modules/data">
Define, query, and control your data with ease.
</Card>
<Card
icon={
<Icon icon="meteor-icons:fingerprint" className="text-fd-primary !size-6" />
}
title="Auth"
href="/modules/auth"
>
Easily implement reliable authentication strategies.
</Card>
<Card
icon={
<Icon icon="flowbite:image-outline" className="text-fd-primary !size-6" />
}
title="Media"
href="/modules/media"
>
Effortlessly manage and serve all your media files.
</Card>
<Card
icon={
<Icon
icon="hugeicons:workflow-square-03"
className="text-fd-primary !size-6"
/>
}
title="Flows"
href="/modules/flows"
>
Design and run workflows with seamless automation.
</Card>
</Cards>

View File

@@ -0,0 +1,11 @@
---
title: "Server"
description: "Effortlessly manage and serve all your files."
tags: ["documentation"]
---
<Callout type="info" title="The documentation is currently a work in progress">
Check back soon — or stay updated on our progress on
[GitHub](https://github.com/bknd-io/bknd) and join the conversation in
[Discord](https://discord.gg/952SFk8Tb8).
</Callout>

View File

@@ -1,23 +1,27 @@
---
title: "Motivation"
description: "Why another backend system?"
icon: Rocket
tags: ["documentation"]
---
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., *"how to do X with Y?"*), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
Creating digital products always requires developing both the backend (the logic) and the frontend (the appearance). Building a backend from scratch demands deep knowledge in areas such as authentication and database management. Using a backend framework can speed up initial development, but it still requires ongoing effort to work within its constraints (e.g., _"how to do X with Y?"_), which can quickly slow you down. Choosing a backend system is a tough decision, as you might not be aware of its limitations until you encounter them.
<Check>
**The solution:** A backend system that only assumes and implements primitive details, integrates into multiple environments, and adheres to industry standards.
</Check>
<CalloutPositive title="The solution">
A backend system that only assumes and implements primitive details,
integrates into multiple environments, and adheres to industry standards.
</CalloutPositive>
For the sake of brevity, let's assume you are looking for a "backend system" rather than dealing with custom implementations yourself. Let's identify the most common challenges:
1. Database lock-in
2. Environment and framework lock-in
3. Deviation from standards (such as `X-Auth` headers for authentication)
4. *Wrong-for-your-use-case* implementations
4. _Wrong-for-your-use-case_ implementations
5. Complex self-hosting
## Database lock-in
As the developer of a backend system, you must make tough decisions, one of which is choosing which database(s) to support. To simplify development, many systems lock you into a single database, leveraging its advanced features.
But isn't the database known to be the hardest part to scale? Isn't more logic moving to the application layer? Haven't NoSQL databases proven this? If you're like me, you may have dipped your toes into the NoSQL world only to quickly return to SQL. SQL is known, predictable, and safe. But what if we could have both? NoSQL offers flexibility and scalability, yet querying it is tedious due to vendor-specific implementations.
@@ -25,21 +29,23 @@ But isn't the database known to be the hardest part to scale? Isn't more logic m
To get the best of both worlds, bknd focuses on the weakest SQL database (SQLite), treating it as a data store and query interface. Schema details and enforcement are moved to the application layer, making it easy to adjust a default value or property length. The added benefit is that any SQL database could theoretically work the same way, and since it's all TypeScript, the same validation logic can be used on both the client and server sidesyou can validate your data before it even reaches your server. It even works without database-enforced referential integrity, as the integrity checks occur on the application layer. This opens the door to NewSQL systems like PlanetScale.
## Environment and framework lock-in
There are backend systems that embed themselves into a specific React framework. This works well until you realize it doesn't support your preferred framework or the new hyped one you're considering switching to. Just like database choices, decisions must be made. The easiest path is to select a single option and let people live with it.
Alternatively, you could develop for the weakest environment (workerd) by strictly using Web APIs, avoiding shortcuts, and implementing certain logic manually because the go-to package is using Node APIs. This isn't always fun, but it's essential. The benefit? It works anywhere JavaScript does.
bknd is the only backend system that not only works with any JavaScript framework but also integrates directly into it. It runs within the framework, enabling a single deployment for your entire app.
*"But isn't it ironic that it forces a JavaScript environment?"* you might ask. And you're right, but it also allows running standalone via CLI or Docker.
_"But isn't it ironic that it forces a JavaScript environment?"_ you might ask. And you're right, but it also allows running standalone via CLI or Docker.
## Deviation from standards
One of the biggest frustrations I've encountered is when software vendors choose custom headers for authentication or implement query parameters in a format they find more suitable—such as unencoded JSON for simplicity. When you are in full control, it's tempting to use a more suitable format, or just use an auth-ish name for the header propertyafter all, it's just a header, right?
The issue is that users may rely on HTTP clients that offer built-in authentication methods, which won't include your custom solution. Custom `SearchParams` implementations might be convenient, but translating them across different environments and languages can be challenging without trial and error.
bknd strives to adhere to web standards as much as possible while offering handy alternatives. Here's an example of the `select` search parameter for retrieving a list of entities:
```bash
/api/data/todos?select=id&select=name # web standard
/api/data/todos?select=id,name # handy alternative
@@ -47,13 +53,14 @@ bknd strives to adhere to web standards as much as possible while offering handy
If you ever find an instance where bknd isn't adhering to standards or could be improved, please feel free to [file an issue](https://github.com/bknd-io/bknd/issues/new). Your feedback is greatly appreciated!
## Wrong-for-your-use-case implementations
If you've ever developed a social chat application, you likely discovered the extensive feature depth required—features we often take for granted. Things like socket connections for single and group chats, partial loading, asset attachments, and emoji reactions. Even more frustrating, these features make the app being considered incomplete until delivered.
The same applies to backend systems. Features such as email sending, password resets, and image transformations are expected. Worse still, you'll receive feedback requesting different email verification methods—PIN codes instead of links, 4-digit codes versus 6-digit ones, or UUIDs like Axiom uses.
Since it's impossible to satisfy all requirements, why implement them at all? *"Because people expect it."* That's fair. But technically, email verification is not a core backend feature—it's business logic. Setting it up involves:
Since it's impossible to satisfy all requirements, why implement them at all? _"Because people expect it."_ That's fair. But technically, email verification is not a core backend feature—it's business logic. Setting it up involves:
1. Adding a `code` and `verified` field to the users' entity and generating a random code on creation.
2. Creating an endpoint to accept the code, retrieve the authenticated user, check the code, clear it, and mark the user as verified.
@@ -62,10 +69,7 @@ Additional security measures, such as short-lived tokens, can be added, but the
Instead of hardcoding such features, bknd offers a powerful event system that supports asynchronous (like webhooks) and synchronous execution, blocking further actions if needed. With integrated workflows (UI coming soon), you can listen to and react to system events, and even map them to endpoints. Since workflows, like everything else in bknd, are JSON-serializable, they're easy to export and import.
## Complex self-hosting
Finally, hosting. It's a business advantage if your system is highly sought after but difficult to self-host, forcing users to opt for your cloud service. The truth is, if it's hard for users, it's also hard for the vendor, which drives up costs.
If you know how to deploy your Next.js, Remix, or Astro application, you can deploy bknd. It's straightforward to deploy using Cloudflare Workers/Pages or with just 28 lines of a Dockerfile. No PhD required.

View File

@@ -0,0 +1,158 @@
---
title: Introduction
icon: Album
tags: ["documentation"]
---
import { Icon } from "@iconify/react";
import { examples } from "@/app/_components/StackBlitz";
Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature-rich backend that runs in any JavaScript environment.
- Instant backend with full REST API
- Built on Web Standards for maximum compatibility
- Multiple run modes (standalone, runtime, framework)
- Official API and React SDK with type-safety
- React elements for auto-configured authentication and media components
## Preview
Here is a preview of **bknd** in StackBlitz:
<StackBlitz {...examples.adminRich} />
<Accordions>
<Accordion title="What's going on?">
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending).
</Accordion>
</Accordions>
## Quickstart
Enter the following command to spin up an instance:
<Tabs groupId='package-manager' persist items={[ 'npm','bun' ]}>
```bash tab="npm"
npx bknd run
```
```bash tab="bun"
bunx bknd run
```
</Tabs>
To learn more about the CLI, check out the [Using the CLI](/usage/cli) guide.
## Start with a Framework/Runtime
Start by using the integration guide for these popular frameworks/runtimes. There will be more in the future, so stay tuned!
<Cards>
<Card icon={<Icon icon="tabler:brand-nextjs" className="text-fd-primary !size-6" />} title="NextJS" href="/integration/nextjs" />
<Card
icon={
<Icon icon="simple-icons:reactrouter" className="text-fd-primary !size-6" />
}
title="React Router"
href="/integration/react-router"
/>
<Card
icon={<Icon icon="simple-icons:astro" className="text-fd-primary !size-6" />}
title="Astro"
href="/integration/astro"
/>
<Card
icon={<Icon icon="tabler:brand-nodejs" className="text-fd-primary !size-6" />}
title="NodeJS"
href="/integration/node"
/>
<Card
icon={
<Icon
icon="devicon-plain:cloudflareworkers"
className="text-fd-primary !size-6"
/>
}
title="Cloudflare"
href="/integration/cloudflare"
/>
<Card
icon={<Icon icon="simple-icons:bun" className="text-fd-primary !size-6" />}
title="Bun"
href="/integration/bun"
/>
<Card
icon={<Icon icon="tabler:lambda" className="text-fd-primary !size-6" />}
title="AWS Lambda"
href="/integration/aws"
/>
<Card
icon={<Icon icon="simple-icons:vitest" className="text-fd-primary !size-6" />}
title="Vite"
href="/integration/vite"
/>
<Card
icon={
<Icon
icon="streamline-logos:docker-logo-solid"
className="text-fd-primary !size-6"
/>
}
title="Docker"
href="/integration/docker"
/>
<Card 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>
</Cards>
## Use your favorite SQL Database
The following databases are currently supported. Request a new integration if your favorite is missing.
<Cards>
<Card icon={<Icon icon="simple-icons:sqlite" className="text-fd-primary !size-6" />} title="SQLite" href="/usage/database#database" />
<Card
icon={<Icon icon="simple-icons:turso" className="text-fd-primary !size-6" />}
title="Turso/LibSQL"
href="/usage/database#sqlite-using-libsql-on-turso"
/>
<Card
icon={
<Icon icon="lineicons:postgresql" className="text-fd-primary !size-6" />
}
title="PostgreSQL"
href="/usage/database#postgresql"
/>
<Card
icon={
<Icon
icon="streamline-plump:database"
className="text-fd-primary !size-6"
/>
}
title="Cloudflare D1"
href="/usage/database#cloudflare-d1"
/>
<Card title="Yours missing?" href="https://github.com/bknd-io/bknd/issues/new">
Create a new issue to request a new database integration.
</Card>
</Cards>

View File

@@ -1,6 +1,7 @@
---
title: 'Using the CLI'
description: 'How to start a bknd instance using the CLI.'
title: "Using the CLI"
description: "How to start a bknd instance using the CLI."
tags: ["documentation"]
---
The bknd package includes a command-line interface (CLI) that allows you to run a bknd instance and perform various tasks.
@@ -10,11 +11,12 @@ npx bknd
```
Here is the output:
```
$ npx bknd
Usage: bknd [options] [command]
⚡ bknd cli v0.13.0
⚡ bknd cli v0.16.0
Options:
-V, --version output the version number
@@ -33,6 +35,7 @@ Commands:
```
## Starting an instance (`run`)
To see all available `run` options, execute `npx bknd run --help`.
```
@@ -51,6 +54,7 @@ Options:
```
To order in which the connection is determined is as follows:
1. `--db-url`
2. `--config` or reading the filesystem looking for `bknd.config.[js|ts|mjs|cjs|json]`
3. `--memory`
@@ -58,6 +62,7 @@ To order in which the connection is determined is as follows:
5. Fallback to file-based database `data.db`
### File-based database
By default, a file-based database `data.db` is used when running without any arguments. You can specify a different file name or path using the `--db-url` option. The database file will be created in the current working directory if it does not exist.
```
@@ -65,6 +70,7 @@ npx bknd run --db-url file:data.db
```
### Using configuration file (`bknd.config.*`)
You can create a configuration file on the working directory that automatically gets picked up: `bknd.config.[js|ts|mjs|cjs|json]`
Here is an example of a `bknd.config.ts` file:
@@ -73,20 +79,20 @@ Here is an example of a `bknd.config.ts` file:
import type { BkndConfig } from "bknd/adapter";
export default {
// you can either specify the connection directly
connection: {
url: "file:data.db",
},
// or use the `app` function which passes the environment variables
app: ({ env }) => ({
connection: {
url: env.DB_URL,
}
})
// you can either specify the connection directly
connection: {
url: "file:data.db",
},
// or use the `app` function which passes the environment variables
app: ({ env }) => ({
connection: {
url: env.DB_URL,
},
}),
} satisfies BkndConfig;
```
The `app` function is useful if you need a cross-platform way to access the environment variables. For example, on Cloudflare Workers, you can only access environment variables inside a request handler. If you're exclusively using a node-like environment, it's safe to access the environment variables directly from `process.env`.
The `app` function is useful if you need a cross-platform way to access the environment variables. For example, on Cloudflare Workers, you can only access environment variables inside a request handler. If you're exclusively using a node-like environment, it's safe to access the environment variables directly from `process.env`.
If you're using `npx bknd run`, make sure to create a file in a file format that `node` can load, otherwise you may run into an error that the file couldn't be found:
@@ -112,21 +118,27 @@ npx tsx node_modules/.bin/bknd run
```
### Turso/LibSQL database
To start an instance with a Turso/LibSQL database, run the following:
```
npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token>
```
The `--db-token` option is optional and only required if the database is protected.
### In-memory database
To start an instance with an ephemeral in-memory database, run the following:
```
npx bknd run --memory
```
Keep in mind that the database is not persisted and will be lost when the process is terminated.
## Generating types (`types`)
To see all available `types` options, execute `npx bknd types --help`.
```
@@ -142,6 +154,7 @@ Options:
```
To generate types for the database, run the following:
```
npx bknd types
```
@@ -149,7 +162,7 @@ npx bknd types
This will generate types for your database schema in `bknd-types.d.ts`. The generated file could look like this:
```typescript bknd-types.d.ts
import type { DB } from "bknd/core";
import type { DB } from "bknd";
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
declare global {
@@ -168,7 +181,7 @@ interface Database {
todos: Todos;
}
declare module "bknd/core" {
declare module "bknd" {
interface DB extends Database {}
}
```
@@ -177,18 +190,17 @@ Make sure to add the generated file in your `tsconfig.json` file:
```json tsconfig.json
{
"include": [
"bknd-types.d.ts"
]
"include": ["bknd-types.d.ts"]
}
```
You can then use the types by importing them from `bknd/core`:
You can then use the types by importing them from `bknd`:
```typescript
import type { DB } from "bknd/core";
import type { DB } from "bknd";
type Todo = DB["todos"];
```
All bknd methods that involve your database schema will be automatically typed.
All bknd methods that involve your database schema will be automatically typed.

View File

@@ -1,11 +1,13 @@
---
title: 'Database'
description: 'Choosing the right database configuration'
title: "Database"
description: "Choosing the right database configuration"
tags: ["documentation"]
---
In order to use **bknd**, you need to prepare access information to your database and potentially install additional dependencies. Connections to the database are managed using Kysely. Therefore, all [its dialects](https://kysely.dev/docs/dialects) are theoretically supported.
In order to use **bknd**, you need to prepare access information to your database and potentially install additional dependencies. Connections to the database are managed using Kysely. Therefore, all [its dialects](https://kysely.dev/docs/dialects) are theoretically supported.
Currently supported and tested databases are:
- SQLite (embedded): Node.js SQLite, Bun SQLite, LibSQL, SQLocal
- SQLite (remote): Turso, Cloudflare D1
- Postgres: Vanilla Postgres, Supabase, Neon, Xata
@@ -13,78 +15,80 @@ Currently supported and tested databases are:
By default, bknd will try to use a SQLite database in-memory. Depending on your runtime, a different SQLite implementation will be used.
## Defining the connection
There are mainly 3 ways to define the connection to your database, when
1. creating an app using `App.create()` or `createApp()`
2. creating an app using a [Framework or Runtime adapter](/integration/introduction)
3. starting a quick instance using the [CLI](/usage/cli#using-configuration-file-bknd-config)
When creating an app using `App.create()` or `createApp()`, you can pass a connection object in the configuration object.
```typescript app.ts
```typescript title="app.ts"
import { createApp } from "bknd";
import { sqlite } from "bknd/adapter/sqlite";
// a connection is required when creating an app like this
const app = createApp({
connection: sqlite({ url: ":memory:" }),
connection: sqlite({ url: ":memory:" }),
});
```
When using an adapter, or using the CLI, bknd will automatically try to use a SQLite implementation depending on the runtime:
```javascript app.js
```javascript title="app.js"
import { serve } from "bknd/adapter/node";
serve({
// connection is optional, but recommended
connection: { url: "file:data.db" },
// connection is optional, but recommended
connection: { url: "file:data.db" },
});
```
You can also pass a connection instance to the `connection` property to explictly use a specific connection.
```javascript app.js
```javascript title="app.js"
import { serve } from "bknd/adapter/node";
import { sqlite } from "bknd/adapter/sqlite";
serve({
connection: sqlite({ url: "file:data.db" }),
connection: sqlite({ url: "file:data.db" }),
});
```
If you're using [`bknd.config.*`](/extending/config), you can specify the connection on the exported object.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
export default {
connection: { url: "file:data.db" },
connection: { url: "file:data.db" },
} as const satisfies BkndConfig;
```
Throughout the documentation, it is assumed you use `bknd.config.ts` to define your connection.
## SQLite
### Using config object
<Callout type="warn">
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running `node -v`, and
[upgrade](https://nodejs.org/en/download/) if necessary.
</Callout>
<Warning>
When run with Node.js, a version of 22 (LTS) or higher is required. Please verify your version by running `node -v`, and [upgrade](https://nodejs.org/en/download/) if necessary.
</Warning>
The `sqlite` adapter is automatically resolved based on the runtime.
The `sqlite` adapter is automatically resolved based on the runtime.
| Runtime | Adapter | In-Memory | File | Remote |
| ------- | ------- | --------- | ---- | ------ |
| Node.js | `node:sqlite` | ✅ | ✅ | ❌ |
| Bun | `bun:sqlite` | ✅ | ✅ | ❌ |
| Cloudflare Worker/Browser/Edge | `libsql` | 🟠 | 🟠 | ✅ |
| Runtime | Adapter | In-Memory | File | Remote |
| ------------------------------ | ------------- | --------- | ---- | ------ |
| Node.js | `node:sqlite` | ✅ | ✅ | ❌ |
| Bun | `bun:sqlite` | ✅ | ✅ | ❌ |
| Cloudflare Worker/Browser/Edge | `libsql` | 🟠 | 🟠 | ✅ |
The bundled version of the `libsql` connection only works with remote databases. However, you can pass in a `Client` from `@libsql/client`, see [LibSQL](#libsql) for more details.
```typescript bknd.config.ts
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
// no connection is required, bknd will use a SQLite database in-memory
@@ -93,51 +97,49 @@ export default {} as const satisfies BkndConfig;
// or explicitly in-memory
export default {
connection: { url: ":memory:" },
connection: { url: ":memory:" },
} as const satisfies BkndConfig;
// or explicitly as a file
export default {
connection: { url: "file:<path/to/your/database.db>" },
connection: { url: "file:<path/to/your/database.db>" },
} as const satisfies BkndConfig;
```
### LibSQL
Turso offers a SQLite-fork called LibSQL that runs a server around your SQLite database. The edge-version of the adapter is included in the bundle (remote only):
```typescript bknd.config.ts
import type { BkndConfig } from "bknd";
import { libsql } from "bknd/data";
```typescript title="bknd.config.ts"
import { libsql, type BkndConfig } from "bknd";
export default {
connection: libsql({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
}),
connection: libsql({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
}),
} as const satisfies BkndConfig;
```
If you wish to use LibSQL as file, in-memory or make use of [Embedded Replicas](https://docs.turso.tech/features/embedded-replicas/introduction), you have to pass in the `Client` from `@libsql/client`:
If you wish to use LibSQL as file, in-memory or make use of [Embedded Replicas](https://docs.turso.tech/features/embedded-replicas/introduction), you have to pass in the `Client` from `@libsql/client`:
```typescript bknd.config.ts
import type { BkndConfig } from "bknd";
import { libsql } from "bknd/data";
```typescript title="bknd.config.ts"
import { libsql, type BkndConfig } from "bknd";
import { createClient } from "@libsql/client";
const client = createClient({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
})
const client = createClient({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
});
export default {
connection: libsql(client),
connection: libsql(client),
} as const satisfies BkndConfig;
```
### Cloudflare D1
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically.
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically.
To manually specify which D1 database to take, you can specify it explicitly:
@@ -145,12 +147,12 @@ To manually specify which D1 database to take, you can specify it explicitly:
import { serve, d1 } from "bknd/adapter/cloudflare";
export default serve<Env>({
app: ({ env }) => d1({ binding: env.D1_BINDING })
app: ({ env }) => d1({ binding: env.D1_BINDING }),
});
```
### SQLocal
To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command:
```bash
@@ -164,14 +166,15 @@ import { createApp } from "bknd";
import { SQLocalConnection } from "@bknd/sqlocal";
const app = createApp({
connection: new SQLocalConnection({
databasePath: ":localStorage:",
verbose: true,
})
connection: new SQLocalConnection({
databasePath: ":localStorage:",
verbose: true,
}),
});
```
## PostgreSQL
To use bknd with Postgres, you need to install the `@bknd/postgres` package. You can do so by running the following command:
```bash
@@ -182,7 +185,7 @@ You can connect to your Postgres database using `pg` or `postgres` dialects. Add
### Using `pg`
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package.
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package.
```js
import { serve } from "bknd/adapter/node";
@@ -190,10 +193,9 @@ import { pg } from "@bknd/postgres";
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
const config = {
connection: pg({
connectionString:
"postgresql://user:password@localhost:5432/database",
}),
connection: pg({
connectionString: "postgresql://user:password@localhost:5432/database",
}),
};
serve(config);
@@ -201,14 +203,14 @@ serve(config);
### Using `postgres`
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package.
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package.
```js
import { serve } from "bknd/adapter/node";
import { postgresJs } from "@bknd/postgres";
serve({
connection: postgresJs("postgresql://user:password@localhost:5432/database"),
connection: postgresJs("postgresql://user:password@localhost:5432/database"),
});
```
@@ -225,9 +227,9 @@ import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection(NeonDialect);
serve({
connection: neon({
connectionString: process.env.NEON,
}),
connection: neon({
connectionString: process.env.NEON,
}),
});
```
@@ -240,73 +242,78 @@ import { buildClient } from "@xata.io/client";
const client = buildClient();
const xata = new client({
databaseURL: process.env.XATA_URL,
apiKey: process.env.XATA_API_KEY,
branch: process.env.XATA_BRANCH,
databaseURL: process.env.XATA_URL,
apiKey: process.env.XATA_API_KEY,
branch: process.env.XATA_BRANCH,
});
const xataConnection = createCustomPostgresConnection(XataDialect, {
supports: {
batching: false,
},
supports: {
batching: false,
},
});
serve({
connection: xataConnection({ xata }),
connection: xataConnection({ xata }),
});
```
## Custom Connection
Creating a custom connection is as easy as extending the `Connection` class and passing constructing a Kysely instance.
```ts
import { createApp } from "bknd";
import { Connection } from "bknd/data";
import { createApp, Connection } from "bknd";
import { Kysely } from "kysely";
class CustomConnection extends Connection {
constructor() {
const kysely = new Kysely(/* ... */);
super(kysely);
}
constructor() {
const kysely = new Kysely(/* ... */);
super(kysely);
}
}
const connection = new CustomConnection();
// e.g. and then, create an instance
const app = createApp({ connection })
const app = createApp({ connection });
```
## Initial Structure
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
<Note>
The initial structure is only respected if the database is empty! If you made updates, ensure to delete the database first, or perform updates through the Admin UI.
</Note>
<Callout type="info">
The initial structure is only respected if the database is empty! If you made
updates, ensure to delete the database first, or perform updates through the
Admin UI.
</Callout>
```typescript
import { createApp } from "bknd";
import { em, entity, text, number } from "bknd/data";
import { createApp, em, entity, text, number } from "bknd";
const schema = em({
posts: entity("posts", {
const schema = em(
{
posts: entity("posts", {
// "id" is automatically added
title: text().required(),
slug: text().required(),
content: text(),
views: number()
}),
comments: entity("comments", {
content: text()
})
// relations and indices are defined separately.
// the first argument are the helper functions, the second the entities.
}, ({ relation, index }, { posts, comments }) => {
relation(comments).manyToOne(posts);
// relation as well as index can be chained!
index(posts).on(["title"]).on(["slug"], true);
});
views: number(),
}),
comments: entity("comments", {
content: text(),
}),
// relations and indices are defined separately.
// the first argument are the helper functions, the second the entities.
},
({ relation, index }, { posts, comments }) => {
relation(comments).manyToOne(posts);
// relation as well as index can be chained!
index(posts).on(["title"]).on(["slug"], true);
},
);
// to get a type from your schema, use:
type Database = (typeof schema)["DB"];
@@ -325,66 +332,81 @@ type Database = (typeof schema)["DB"];
// pass the schema to the app
const app = createApp({
connection: { /* ... */ },
initialConfig: {
data: schema.toJSON()
}
connection: {
/* ... */
},
initialConfig: {
data: schema.toJSON(),
},
});
```
Note that we didn't add relational fields directly to the entity, but instead defined them afterwards. That is because the relations are managed outside the entity scope to have an unified expierence for all kinds of relations (e.g. many-to-many).
<Note>
Defined relations are currently not part of the produced types for the structure. We're working on that, but in the meantime, you can define them manually.
</Note>
<Callout type="info">
Defined relations are currently not part of the produced types for the
structure. We're working on that, but in the meantime, you can define them
manually.
</Callout>
### Type completion
To get type completion, there are two options:
1. Use the CLI to [generate the types](/usage/cli#generating-types-types)
2. If you have an initial structure created with the prototype functions, you can extend the `DB` interface with your own schema.
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
All entity related functions use the types defined in `DB` from `bknd`. To get type completion, you can extend that interface with your own schema:
```typescript
import { em } from "bknd/data";
import { em } from "bknd";
import { Api } from "bknd/client";
const schema = em({ /* ... */ });
const schema = em({
/* ... */
});
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
declare module "bknd" {
interface DB extends Database {}
}
const api = new Api({ /* ... */ });
const { data: posts } = await api.data.readMany("posts", {})
const api = new Api({
/* ... */
});
const { data: posts } = await api.data.readMany("posts", {});
// `posts` is now typed as Database["posts"]
```
The type completion is available for the API as well as all provided [React hooks](/usage/react).
### Seeding the database
To seed your database with initial data, you can pass a `seed` function to the configuration. It
provides the `ModuleBuildContext` as the first argument.
<Note>
Note that the seed function will only be executed on app's first boot. If a configuration
already exists in the database, it will not be executed.
</Note>
<Callout type="info">
Note that the seed function will only be executed on app's first boot. If a
configuration already exists in the database, it will not be executed.
</Callout>
```typescript
import { createApp, type ModuleBuildContext } from "bknd";
const app = createApp({
connection: { /* ... */ },
initialConfig: { /* ... */ },
options: {
seed: async (ctx: ModuleBuildContext) => {
await ctx.em.mutator("posts").insertMany([
{ title: "First post", slug: "first-post", content: "..." },
{ title: "Second post", slug: "second-post" }
]);
}
}
connection: {
/* ... */
},
initialConfig: {
/* ... */
},
options: {
seed: async (ctx: ModuleBuildContext) => {
await ctx.em.mutator("posts").insertMany([
{ title: "First post", slug: "first-post", content: "..." },
{ title: "Second post", slug: "second-post" },
]);
},
},
});
```
```

View File

@@ -1,24 +1,28 @@
---
title: "React Elements"
description: "Speed up your frontend development"
tags: ["documentation"]
---
Not only creating and maintaing a backend is time-consuming, but also integrating it into your frontend can be a hassle. With `bknd/elements`, you can easily add media uploads and authentication forms to your app without having to figure out API details.
<Note>
In order to use these exported elements, make sure to wrap your app inside `ClientProvider`. See the [React Setup](/usage/react#setup) for more information.
</Note>
<Callout type="info">
In order to use these exported elements, make sure to wrap your app inside
`ClientProvider`. See the [React Setup](/usage/react#setup) for more
information.
</Callout>
# Media
## Media
### Media.Dropzone
## Media.Dropzone
The `Media.Dropzone` element allows retrieving from and uploading media items to your bknd instance. Without any properties specified, it will behave similar to your media library inside the bknd Admin UI. Here is how to get the last 10 items:
```tsx
import { Media } from "bknd/elements"
import { Media } from "bknd/elements";
export default function MediaGallery() {
return <Media.Dropzone query={{ limit: 10, sort: "-id" }} />
return <Media.Dropzone query={{ limit: 10, sort: "-id" }} />;
}
```
@@ -28,15 +32,18 @@ Since you can also upload media to a specific entity, you can also point that `D
import { Media } from "bknd/elements";
export default function UserAvatar() {
return <Media.Dropzone
return (
<Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
/>
/>
);
}
```
### Props
#### Props
- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects.
- `entity?: { name: string; id: number; field: string }`: If given, the initial media items fetched will be from this entity.
- `query?: RepoQueryIn`: Query to filter the media items.
@@ -48,88 +55,97 @@ export default function UserAvatar() {
- `onUploaded?: (file: FileState) => void`: Callback when a file is uploaded.
- `placeholder?: { show?: boolean; text?: string }`: Placeholder text to show when no media items are present.
### Customize Rendering
#### Customize Rendering
You can also customize the rendering of the media items and its uploading by passing a react element as a child. Here is an example of a custom `Media.Dropzone` that renders an user avatar (styled using tailwind):
```tsx
import { Media, useMediaDropzone, useMediaDropzoneState } from "bknd/elements";
export default function CustomUserAvatar() {
return <Media.Dropzone
return (
<Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
>
>
<CustomUserAvatar />
</Media.Dropzone>
</Media.Dropzone>
);
}
function CustomUserAvatar() {
const {
wrapperRef,
inputProps,
showPlaceholder,
actions: { openFileInput }
} = useMediaDropzone();
const { files: [ file ], isOver, isOverAccepted } = useMediaDropzoneState();
const {
wrapperRef,
inputProps,
showPlaceholder,
actions: { openFileInput },
} = useMediaDropzone();
const {
files: [file],
isOver,
isOverAccepted,
} = useMediaDropzoneState();
return (
<div
ref={wrapperRef}
className="size-32 rounded-full border border-gray-200 flex justify-center items-center leading-none overflow-hidden"
>
<div className="hidden">
<input {...inputProps} />
</div>
{showPlaceholder && <>{isOver && isOverAccepted ? "let it drop" : "drop here"}</>}
{file && (
<Media.Preview
file={file}
className="object-cover w-full h-full"
onClick={openFileInput}
/>
)}
return (
<div
ref={wrapperRef}
className="size-32 rounded-full border border-gray-200 flex justify-center items-center leading-none overflow-hidden"
>
<div className="hidden">
<input {...inputProps} />
</div>
);
{showPlaceholder && (
<>{isOver && isOverAccepted ? "let it drop" : "drop here"}</>
)}
{file && (
<Media.Preview
file={file}
className="object-cover w-full h-full"
onClick={openFileInput}
/>
)}
</div>
);
}
```
# Auth
## Auth
Adding authentication to your app with bknd is as easy as adding a `<form method="POST" />` with an action pointing to the action (`login` or `register`) to the strategy you want to use, e.g. for the password strategy, use `/api/auth/password/login`. But to make it even easier, you can use the `Auth.*` elements.
## `Auth.Screen`
### `Auth.Screen`
The `Auth.Screen` element is a wrapper around the `Auth.Form` element that provides a full page screen. The current layout is admittedly very basic, but there will be more customization options in the future.
```tsx
import { Auth } from "bknd/elements"
import { Auth } from "bknd/elements";
export default function LoginScreen() {
return <Auth.Screen action="login" />
return <Auth.Screen action="login" />;
}
```
### Props
Note that this component doesn't require any strategy-specific information, as it gathers it itself.
#### Props
Note that this component doesn't require any strategy-specific information, as it gathers it itself.
- `action: "login" | "register"`: The action to perform.
- `method?: "POST" | "GET"`: The method to use for the form.
### `Auth.Form`
## `Auth.Form`
If you only wish to render the form itself without the screen, you can use the `Auth.Form` element. Unlike the `Auth.Screen`, this element requires the `strategy` prop to be set to the strategy you want to use. You can either specify it manually, use use the exported hook `useAuthStrategies()` for fetch them from your bknd instance.
```tsx
import { Auth, useAuthStrategies } from "bknd/elements"
import { Auth, useAuthStrategies } from "bknd/elements";
export default function LoginForm() {
const { strategies, basepath, loading } = useAuthStrategies();
if (loading) return null;
return <Auth.Form
action="login"
strategies={strategies}
basepath={basepath}
/>
const { strategies, basepath, loading } = useAuthStrategies();
if (loading) return null;
return (
<Auth.Form action="login" strategies={strategies} basepath={basepath} />
);
}
```
```

View File

@@ -1,20 +1,23 @@
---
title: 'Introduction'
description: 'Setting up bknd'
title: "Introduction"
description: "Setting up bknd"
tags: ["documentation"]
---
There are several methods to get **bknd** up and running. You can choose between these options:
1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started.
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
native server and serves the UI assets statically from `node_modules`.
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
native server and serves the UI assets statically from `node_modules`.
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
[Astro](/integration/astro) or [Remix](/integration/remix).
[Astro](/integration/astro) or [Remix](/integration/remix).
There is also a fourth option, which is running it inside a
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
## Basic setup
Regardless of the method you choose, at the end all adapters come down to the actual
instantiation of the `App`, which in raw looks like this:
@@ -22,7 +25,9 @@ instantiation of the `App`, which in raw looks like this:
import { createApp, type CreateAppConfig } from "bknd";
// create the app
const config = { /* ... */ } satisfies CreateAppConfig;
const config = {
/* ... */
} satisfies CreateAppConfig;
const app = createApp(config);
// build the app
@@ -36,40 +41,41 @@ In Web API compliant environments, all you have to do is to default exporting th
implements the `Fetch` API.
## Configuration (`CreateAppConfig`)
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext } from "bknd";
import type { Connection } from "bknd/data";
import type { App, InitialModuleConfigs, ModuleBuildContext, Connection } from "bknd";
import type { Config } from "@libsql/client";
type AppPlugin = (app: App) => Promise<void> | void;
type ManagerOptions = {
basePath?: string;
trustFetched?: boolean;
onFirstBoot?: () => Promise<void>;
seed?: (ctx: ModuleBuildContext) => Promise<void>;
basePath?: string;
trustFetched?: boolean;
onFirstBoot?: () => Promise<void>;
seed?: (ctx: ModuleBuildContext) => Promise<void>;
};
type CreateAppConfig = {
connection?:
| Connection
| Config;
initialConfig?: InitialModuleConfigs;
options?: {
plugins?: AppPlugin[];
manager?: ManagerOptions
};
connection?: Connection | Config;
initialConfig?: InitialModuleConfigs;
options?: {
plugins?: AppPlugin[];
manager?: ManagerOptions;
};
};
```
### `connection`
The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class.
```ts
const connection = {
url: "<url>",
authToken: "<token>"
}
url: "<url>",
authToken: "<token>",
};
```
Alternatively, you can pass an instance of a `Connection` class directly,
@@ -78,6 +84,7 @@ see [Custom Connection](/usage/database#custom-connection) as a reference.
If the connection object is omitted, the app will try to use an in-memory database.
### `initialConfig`
As [initial configuration](/usage/database#initial-structure), you can either pass a partial configuration object or a complete one
with a version number. The version number is used to automatically migrate the configuration up
to the latest version upon boot. The default configuration looks like this:
@@ -92,8 +99,13 @@ to the latest version upon boot. The default configuration looks like this:
},
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE" ],
"allow_headers": ["Content-Type", "Content-Length", "Authorization", "Accept"]
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
]
}
},
"data": {
@@ -146,11 +158,13 @@ to the latest version upon boot. The default configuration looks like this:
```
You can use the CLI to get the default configuration:
```sh
npx bknd config --pretty
```
To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
```sh
npx bknd schema
```
@@ -158,6 +172,7 @@ npx bknd schema
To create an initial data structure, you can use helpers [described here](/usage/database#initial-structure).
### `options.plugins`
The `plugins` property is an array of functions that are called after the app has been built,
but before its event is emitted. This is useful for adding custom routes or other functionality.
A simple plugin that adds a custom route looks like this:
@@ -166,7 +181,7 @@ A simple plugin that adds a custom route looks like this:
import type { AppPlugin } from "bknd";
export const myPlugin: AppPlugin = (app) => {
app.server.get("/hello", (c) => c.json({ hello: "world" }));
app.server.get("/hello", (c) => c.json({ hello: "world" }));
};
```
@@ -175,35 +190,39 @@ structure, add custom middlewares, respond to or add events, etc. Plugins are ve
make sure to only run trusted ones.
### `options.seed`
The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object:
```ts
type ModuleBuildContext = {
connection: Connection;
server: Hono;
em: EntityManager;
emgr: EventManager;
guard: Guard;
connection: Connection;
server: Hono;
em: EntityManager;
emgr: EventManager;
guard: Guard;
};
const seed = async (ctx: ModuleBuildContext) => {
// seed the database
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false }
]);
// seed the database
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
};
```
### `options.manager`
This object is passed to the `ModuleManager` which is responsible for:
- validating and maintaining configuration of all modules
- building all modules (data, auth, media, flows)
- maintaining the `ModuleBuildContext` used by the modules
The `options.manager` object has the following properties:
- `basePath` (`string`): The base path for the Hono instance. This is used to prefix all routes.
- `trustFetched` (`boolean`): If set to `true`, the app will not perform any validity checks for
the given or fetched configuration.
the given or fetched configuration.
- `onFirstBoot` (`() => Promise<void>`): A function that is called when the app is booted for
the first time.
the first time.

View File

@@ -1,9 +1,11 @@
---
title: 'SDK (React)'
description: 'Use the bknd SDK for React'
title: "SDK (React)"
description: "Use the bknd SDK for React"
tags: ["documentation"]
---
There are 4 useful hooks to work with your backend:
1. simple hooks which are solely based on the [API](/usage/sdk):
- [`useApi`](#useapi)
- [`useEntity`](#useentity)
@@ -11,69 +13,74 @@ There are 4 useful hooks to work with your backend:
- [`useApiQuery`](#useapiquery)
- [`useEntityQuery`](#useentityquery)
## Setup
In order to use them, make sure you wrap your `<App />` inside `<ClientProvider />`, so that these hooks point to your bknd instance:
```tsx
import { ClientProvider } from "bknd/client";
export default function App() {
return <ClientProvider>
{/* your app */}
</ClientProvider>
return <ClientProvider>{/* your app */}</ClientProvider>;
}
```
For all other examples below, we'll assume that your app is wrapped inside the `ClientProvider`.
## `useApi()`
To use the simple hook that returns the Api, you can use:
```tsx
import { useApi } from "bknd/client";
export default function App() {
const api = useApi();
// ...
const api = useApi();
// ...
}
```
## `useApiQuery()`
This hook wraps the API class in an SWR hook for convenience. You can use any API endpoint
supported, like so:
```tsx
import { useApiQuery } from "bknd/client";
export default function App() {
const { data, ...swr } = useApiQuery((api) => api.data.readMany("comments"));
const { data, ...swr } = useApiQuery((api) => api.data.readMany("comments"));
if (swr.error) return <div>Error</div>
if (swr.isLoading) return <div>Loading...</div>
if (swr.error) return <div>Error</div>;
if (swr.isLoading) return <div>Loading...</div>;
return <pre>{JSON.stringify(data, null, 2)}</pre>
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
### Props
* `selector: (api: Api) => FetchPromise`
The first parameter is a selector function that provides an Api instance and expects an
endpoint function to be returned.
- `selector: (api: Api) => FetchPromise`
* `options`: optional object that inherits from `SWRConfiguration`
The first parameter is a selector function that provides an Api instance and expects an
endpoint function to be returned.
- `options`: optional object that inherits from `SWRConfiguration`
```ts
type Options <Data> = import("swr").SWRConfiguration & {
enabled? : boolean;
refine? : (data: Data) => Data | any;
}
type Options<Data> = import("swr").SWRConfiguration & {
enabled?: boolean;
refine?: (data: Data) => Data | any;
};
```
* `enabled`: Determines whether this hook should trigger a fetch of the data or not.
* `refine`: Optional refinement that is called after a response from the API has been
received. Useful to omit irrelevant data from the response (see example below).
received. Useful to omit irrelevant data from the response (see example below).
### Using mutations
To query and mutate data using this hook, you can leverage the parameters returned. In the
following example we'll also use a `refine` function as well as `revalidateOnFocus` (option from
`SWRConfiguration`) so that our data keeps updating on window focus change.
@@ -83,132 +90,151 @@ import { useEffect, useState } from "react";
import { useApiQuery } from "bknd/client";
export default function App() {
const [text, setText] = useState("");
const { data, api, mutate, ...q } = useApiQuery(
(api) => api.data.readOne("comments", 1),
{
// filter to a subset of the response
refine: (data) => data.data,
revalidateOnFocus: true
}
);
const [text, setText] = useState("");
const { data, api, mutate, ...q } = useApiQuery(
(api) => api.data.readOne("comments", 1),
{
// filter to a subset of the response
refine: (data) => data.data,
revalidateOnFocus: true,
},
);
const comment = data ? data : null;
const comment = data ? data : null;
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
if (q.error) return <div>Error</div>
if (q.isLoading) return <div>Loading...</div>
if (q.error) return <div>Error</div>;
if (q.isLoading) return <div>Loading...</div>;
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
// this will automatically revalidate the query
await mutate(async () => {
const res = await api.data.updateOne("comments", comment.id, {
content: text
});
return res.data;
});
// this will automatically revalidate the query
await mutate(async () => {
const res = await api.data.updateOne("comments", comment.id, {
content: text,
});
return res.data;
});
return false;
}}
>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Update</button>
</form>
);
return false;
}}
>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Update</button>
</form>
);
}
```
## `useEntity()`
This hook wraps the endpoints of `DataApi` and returns CRUD options as parameters:
```tsx
import { useState, useEffect } from "react";
import { useEntity } from "bknd/client";
export default function App() {
const [data, setData] = useState<any>();
const { create, read, update, _delete } = useEntity("comments", 1);
const [data, setData] = useState<any>();
const { create, read, update, _delete } = useEntity("comments", 1);
useEffect(() => {
read().then(setData);
}, []);
useEffect(() => {
read().then(setData);
}, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
If you only supply the entity name as string without an ID, the `read` method will fetch a list
of entities instead of a single entry.
### Props
Following props are available when using `useEntityQuery([entity], [id?])`:
- `entity: string`: Specify the table name of the entity
- `id?: number | string`: If an id given, it will fetch a single entry, otherwise a list
### Returned actions
The following actions are returned from this hook:
- `create: (input: object)`: Create a new entry
- `read: (query: Partial<RepoQuery> = {})`: If an id was given,
it returns a single item, otherwise a list
it returns a single item, otherwise a list
- `update: (input: object, id?: number | string)`: If an id was given, the id parameter is
optional. Updates the given entry partially.
optional. Updates the given entry partially.
- `_delete: (id?: number | string)`: If an id was given, the id parameter is
optional. Deletes the given entry.
optional. Deletes the given entry.
## `useEntityQuery()`
This hook wraps the actions from `useEntity` around `SWR`. The previous example would look like
this:
```tsx
import { useEntityQuery } from "bknd/client";
export default function App() {
const { data } = useEntityQuery("comments", 1);
const { data } = useEntityQuery("comments", 1);
return <pre>{JSON.stringify(data, null, 2)}</pre>
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
### Using mutations
All actions returned from `useEntityQuery` are conveniently wrapped around the `mutate` function,
so you don't have think about this:
```tsx
import { useState, useEffect } from "react";
import { useEntityQuery } from "bknd/client";
export default function App() {
const [text, setText] = useState("");
const { data, update, ...q } = useEntityQuery("comments", 1);
const [text, setText] = useState("");
const { data, update, ...q } = useEntityQuery("comments", 1);
const comment = data ? data : null;
const comment = data ? data : null;
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
if (q.error) return <div>Error</div>
if (q.isLoading) return <div>Loading...</div>
if (q.error) return <div>Error</div>;
if (q.isLoading) return <div>Loading...</div>;
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
// this will automatically revalidate the query
await update({ content: text });
// this will automatically revalidate the query
await update({ content: text });
return false;
}}
>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Update</button>
</form>
);
return false;
}}
>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Update</button>
</form>
);
}
```

View File

@@ -1,9 +1,11 @@
---
title: 'SDK (TypeScript)'
description: 'Use the bknd SDK in TypeScript'
title: "SDK (TypeScript)"
description: "Use the bknd SDK in TypeScript"
tags: ["documentation"]
---
To start using the bknd API, start by creating a new API instance:
```ts
import { Api } from "bknd/client";
@@ -17,9 +19,11 @@ The `Api` class is the main entry point for interacting with the bknd API. It pr
for all available modules described below.
## Setup
You can initialize an API instance by providing the `Request` object, or manually specifying the details such as `host` and `token`.
### Using the `Request` object
The recommended way to create an API instance is by passing the current `Request` object. This will automatically point the API to your current instance and extract the token from the headers (either from cookies or `Authorization` header):
```ts
@@ -40,23 +44,25 @@ import { Api } from "bknd/client";
let request: Request;
const api = new Api({
host: "https://<your-endpoint>",
request,
host: "https://<your-endpoint>",
request,
});
```
### Using the `token` option
If you want to have an API instance that is using a different token, e.g. an admin token, you can create it by specifying the `host` and `token` option:
```ts
import { Api } from "bknd/client";
const api = new Api({
host: "https://<your-endpoint>",
token: "<your-token>"
host: "https://<your-endpoint>",
token: "<your-token>",
});
```
### Using a local API
In case the place where you're using the API is the same as your bknd instance (e.g. when using it embedded in a React framework), you can specify a `fetcher` option to point to your bknd app. This way, requests won't travel over the network and instead processed locally:
```ts
@@ -67,141 +73,171 @@ import { Api } from "bknd/client";
let app: App;
const api = new Api({
fetcher: app.server.request as typeof fetch,
// specify `host` and `token` or `request`
fetcher: app.server.request as typeof fetch,
// specify `host` and `token` or `request`
});
```
## Data (`api.data`)
Access the `Data` specific API methods at `api.data`.
### `data.readMany([entity], [query])`
To retrieve a list of records from an entity, use the `readMany` method:
```ts
const { data } = await api.data.readMany("posts");
```
You can also add additional query instructions:
```ts
const { data } = await api.data.readMany("posts", {
limit: 10,
offset: 0,
select: ["id", "title", "views"],
with: {
// join last 2 comments
comments: {
with: {
// along with the comments' user
users: {}
},
limit: 2,
sort: "-id"
limit: 10,
offset: 0,
select: ["id", "title", "views"],
with: {
// join last 2 comments
comments: {
with: {
// along with the comments' user
users: {},
},
// also get the first 2 images, but only the path
images: {
select: ["path"],
limit: 2
}
},
where: {
// same as '{ title: { $eg: "Hello, World!" } }'
title: "Hello, World!",
// only with views greater than 100
views: {
$gt: 100
}
},
// sort by views descending (without "-" would be ascending)
sort: "-views"
limit: 2,
sort: "-id",
},
// also get the first 2 images, but only the path
images: {
select: ["path"],
limit: 2,
},
},
where: {
// same as '{ title: { $eg: "Hello, World!" } }'
title: "Hello, World!",
// only with views greater than 100
views: {
$gt: 100,
},
},
// sort by views descending (without "-" would be ascending)
sort: "-views",
});
```
The `with` property automatically adds the related entries to the response.
### `data.readOne([entity], [id])`
To retrieve a single record from an entity, use the `readOne` method:
```ts
const { data } = await api.data.readOne("posts", 1);
```
### `data.createOne([entity], [data])`
To create a single record of an entity, use the `createOne` method:
```ts
const { data } = await api.data.createOne("posts", {
title: "Hello, World!",
content: "This is a test post.",
views: 0
title: "Hello, World!",
content: "This is a test post.",
views: 0,
});
```
### `data.createMany([entity], [data])`
To create many records of an entity, use the `createMany` method:
```ts
const { data } = await api.data.createMany("posts", [
{ title: "Hello, World!" },
{ title: "Again, Hello." },
{ title: "Hello, World!" },
{ title: "Again, Hello." },
]);
```
### `data.updateOne([entity], [id], [data])`
To update a single record of an entity, use the `updateOne` method:
```ts
const { data } = await api.data.updateOne("posts", 1, {
views: 1
views: 1,
});
```
### `data.updateMany([entity], [where], [update])`
To update many records of an entity, use the `updateMany` method:
```ts
const { data } = await api.data.updateMany("posts", { views: { $gt: 1 } }, {
title: "viewed more than once"
});
const { data } = await api.data.updateMany(
"posts",
{ views: { $gt: 1 } },
{
title: "viewed more than once",
},
);
```
### `data.deleteOne([entity], [id])`
To delete a single record of an entity, use the `deleteOne` method:
```ts
const { data } = await api.data.deleteOne("posts", 1);
```
### `data.deleteMany([entity], [where])`
To delete many records of an entity, use the `deleteMany` method:
```ts
const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } });
```
## Auth (`api.auth`)
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
API will automatically save the token and use it for subsequent requests.
### `auth.strategies()`
To retrieve the available authentication strategies, use the `strategies` method:
```ts
const { data } = await api.auth.strategies();
```
### `auth.login([strategy], [input])`
To log in with a password, use the `login` method:
```ts
const { data } = await api.auth.login("password", {
email: "...",
password: "..."
email: "...",
password: "...",
});
```
### `auth.register([strategy], [input])`
To register with a password, use the `register` method:
```ts
const { data } = await api.auth.register("password", {
email: "...",
password: "..."
email: "...",
password: "...",
});
```
### `auth.me()`
To retrieve the current user, use the `me` method:
```ts
const { data } = await api.auth.me();
```

View File

@@ -0,0 +1,15 @@
---
title: Delete many
full: true
_openapi:
method: DELETE
route: /api/data/entity/{entity}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}","method":"delete"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Delete one
full: true
_openapi:
method: DELETE
route: /api/data/entity/{entity}/{id}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/{id}","method":"delete"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Retrieve data configuration
full: true
_openapi:
method: GET
route: /api/data
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Read many
full: true
_openapi:
method: GET
route: /api/data/entity/{entity}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Read one
full: true
_openapi:
method: GET
route: /api/data/entity/{entity}/{id}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/{id}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Read many by reference
full: true
_openapi:
method: GET
route: /api/data/entity/{entity}/{id}/{reference}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/{id}/{reference}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Retrieve entity info
full: true
_openapi:
method: GET
route: /api/data/info/{entity}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/info/{entity}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Retrieve data schema
full: true
_openapi:
method: GET
route: /api/data/schema.json
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/schema.json","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Retrieve entity schema
full: true
_openapi:
method: GET
route: /api/data/schemas/{entity}/{context}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/schemas/{entity}/{context}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Sync database schema
full: true
_openapi:
method: GET
route: /api/data/sync
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/sync","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Update many
full: true
_openapi:
method: PATCH
route: /api/data/entity/{entity}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}","method":"patch"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Update one
full: true
_openapi:
method: PATCH
route: /api/data/entity/{entity}/{id}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/{id}","method":"patch"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Insert one or many
full: true
_openapi:
method: POST
route: /api/data/entity/{entity}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}","method":"post"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Count entities
full: true
_openapi:
method: POST
route: /api/data/entity/{entity}/fn/count
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/fn/count","method":"post"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Check if entity exists
full: true
_openapi:
method: POST
route: /api/data/entity/{entity}/fn/exists
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/fn/exists","method":"post"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Query entities
full: true
_openapi:
method: POST
route: /api/data/entity/{entity}/query
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/data/entity/{entity}/query","method":"post"}]} webhooks={[]} hasHead={false} />

View File

@@ -1,14 +1,17 @@
---
title: 'Introduction'
description: 'Example section for showcasing your API endpoints'
title: "Introduction"
description: "Example section for showcasing your API endpoints"
icon: Album
tags: ["openapi"]
---
<Warning>
<Callout type="warn">
You need to make sure to point your request to your API server.
</Warning>
<Note>
This section is a work in progress. Updates will be made soon.
</Note>
</Callout>
<Callout type="info">
This section is a work in progress. Updates will be made soon.
</Callout>
## Authentication

View File

@@ -0,0 +1,7 @@
{
"title": "API Reference",
"description": "Specs for endpoints and options",
"icon": "Webhook",
"root": true,
"pages": ["introduction", "..."]
}

View File

@@ -0,0 +1,15 @@
---
title: Get the config for a module
full: true
_openapi:
method: GET
route: /api/system/config/{module}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/system/config/{module}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Get the raw config
full: true
_openapi:
method: GET
route: /api/system/config/raw
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/system/config/raw","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Get the server info
full: true
_openapi:
method: GET
route: /api/system/info
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/system/info","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Ping the server
full: true
_openapi:
method: GET
route: /api/system/ping
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/system/ping","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Get the schema for a module
full: true
_openapi:
method: GET
route: /api/system/schema/{module}
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/system/schema/{module}","method":"get"}]} webhooks={[]} hasHead={false} />

View File

@@ -0,0 +1,15 @@
---
title: Build the app
full: true
_openapi:
method: POST
route: /api/system/build
toc: []
structuredData:
headings: []
contents: []
---
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
<APIPage document={"openapi.json"} operations={[{"path":"/api/system/build","method":"post"}]} webhooks={[]} hasHead={false} />

Some files were not shown because too many files have changed in this diff Show More