updated nextjs example

This commit is contained in:
dswbx
2025-03-04 14:39:32 +01:00
parent ab73b02138
commit 4f52537ea0
15 changed files with 440 additions and 211 deletions

View File

@@ -1,5 +1,6 @@
import type { App } from "bknd"; import type { App } from "bknd";
import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter"; import { type FrameworkBkndConfig, createFrameworkApp } from "bknd/adapter";
import { getRuntimeKey, isNode } from "core/utils";
export type NextjsBkndConfig = FrameworkBkndConfig & { export type NextjsBkndConfig = FrameworkBkndConfig & {
cleanRequest?: { searchParams?: string[] }; cleanRequest?: { searchParams?: string[] };
@@ -31,6 +32,16 @@ function getCleanRequest(req: Request, cleanRequest: NextjsBkndConfig["cleanRequ
const url = new URL(req.url); const url = new URL(req.url);
cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k)); cleanRequest?.searchParams?.forEach((k) => url.searchParams.delete(k));
if (isNode()) {
return new Request(url.toString(), {
method: req.method,
headers: req.headers,
body: req.body,
// @ts-ignore
duplex: "half",
});
}
return new Request(url.toString(), { return new Request(url.toString(), {
method: req.method, method: req.method,
headers: req.headers, headers: req.headers,

View File

@@ -39,3 +39,11 @@ export function runtimeSupports(feature: keyof typeof features) {
return features[feature]; return features[feature];
} }
export function isNode() {
try {
return global?.process?.release?.name === "node";
} catch (e) {
return false;
}
}

View File

@@ -23,78 +23,97 @@ To get started with Next.js and bknd you can either install the package manually
</Tabs> </Tabs>
## Serve the API ## Serve the API
``` tsx Create a helper file to instantiate the bknd instance and retrieve the API:
// pages/api/[...route].ts
import { serve } from "bknd/adapter/nextjs"; ```ts src/bknd.ts
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
import { headers } from "next/headers";
export const config = { export const config = {
runtime: "edge", // or "experimental-edge", depending on your nextjs version
unstable_allowDynamic: ["**/*.js"]
};
export default serve({
connection: { connection: {
url: process.env.DB_URL!, url: "file:data.db"
authToken: process.env.DB_AUTH_TOKEN! },
} as const satisfies NextjsBkndConfig;
export async function getApp() {
return await getBkndApp(config);
}
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;
}
return app.getApi();
} }
});
``` ```
For more information about the connection object, refer to the [Database](/usage/database) guide. For more information about the connection object, refer to the [Database](/usage/database) guide.
## Enabling the Admin UI Now to expose the API, create a catch-all route file at `src/api/[[...bknd]]/route.ts`:
Create a file `[[...admin]].tsx` inside the `pages/admin` folder: ```ts src/api/[[...bknd]]/route.ts
```tsx import { config } from "@/bknd";
// pages/admin/[[...admin]].tsx import { serve } from "bknd/adapter/nextjs";
import type { InferGetServerSidePropsType as InferProps } from "next";
import { withApi } from "bknd/adapter/nextjs";
import dynamic from "next/dynamic";
import "bknd/dist/styles.css";
const Admin = dynamic(() => import("bknd/ui").then((mod) => mod.Admin), { // optionally, you can set the runtime to edge for better performance
ssr: false, export const runtime = "edge";
});
export const getServerSideProps = withApi(async (context) => { const handler = serve({
return { ...config,
props: { cleanRequest: {
user: context.api.getUser(), // 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 default function AdminPage({ user }: InferProps<typeof getServerSideProps>) { export const GET = handler;
if (typeof document === "undefined") return null; export const POST = handler;
return <Admin export const PUT = handler;
withProvider={{ user }} export const PATCH = handler;
config={{ basepath: "/admin", logo_return_path: "/../" }} export const DELETE = handler;
/>;
}
``` ```
## Example usage of the API in pages dir ## Enabling the Admin UI
Using pages dir, you need to wrap the `getServerSideProps` function with `withApi` to get access Create a page at `admin/[[...admin]]/page.tsx`:
to the API. With the API, you can query the database or retrieve the authentication status: ```tsx admin/[[...admin]]/page.tsx
```tsx import { Admin } from "bknd/ui";
import { withApi } from "bknd/adapter/nextjs"; import { getApi } from "@/bknd";
import type { InferGetServerSidePropsType as InferProps } from "next"; import "bknd/dist/styles.css";
export const getServerSideProps = withApi(async (context) => { export default async function AdminPage() {
const { data = [] } = await context.api.data.readMany("todos"); // make sure to verify auth using headers
const user = context.api.getUser(); const api = await getApi({ verify: true });
return { props: { data, user } };
});
export default function Home(props: InferProps<typeof getServerSideProps>) {
const { data, user } = props;
return ( return (
<div> <Admin
<h1>Data</h1> withProvider={{ user: api.getUser() }}
<pre>{JSON.stringify(data, null, 2)}</pre> config={{
basepath: "/admin",
<h1>User</h1> logo_return_path: "/../",
<pre>{JSON.stringify(user, null, 2)}</pre> color_scheme: "system",
</div> }}
/>
); );
} }
``` ```
## 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
import { getApi } from "@/bknd";
export default async function Home() {
const api = await getApi();
const { data: todos } = await api.data.readMany("todos", { limit: 5 });
return <ul>
{todos.map((todo) => (
<li key={String(todo.id)}>{todo.title}</li>
))}
</ul>
}
```

View File

@@ -24,7 +24,8 @@ To get started with Remix and bknd you can either install the package manually,
## Serve the API ## Serve the API
Since Remix doesn't support middleware yet, we need a helper file to initialize the App to import from. Create a new file at `app/bknd.ts`: Create a helper file to instantiate the bknd instance and retrieve the API:
```ts app/bknd.ts ```ts app/bknd.ts
import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix"; import { type RemixBkndConfig, getApp as getBkndApp } from "bknd/adapter/remix";
@@ -49,6 +50,7 @@ export async function getApi(args?: { request: Request }) {
return app.getApi(); 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`: Create a new api splat route file at `app/routes/api.$.ts`:
```ts app/routes/api.$.ts ```ts app/routes/api.$.ts
@@ -62,7 +64,6 @@ const handler = async (args: { request: Request }) => {
export const loader = handler; export const loader = handler;
export const action = handler; export const action = handler;
``` ```
For more information about the connection object, refer to the [Database](/usage/database) guide.
Now make sure that you wrap your root layout with the `ClientProvider` so that all components share the same context. Also add the user context to both the `Outlet` and the provider: Now make sure that you wrap your root layout with the `ClientProvider` so that all components share the same context. Also add the user context to both the `Outlet` and the provider:
```tsx app/root.tsx ```tsx app/root.tsx

View File

@@ -14,8 +14,8 @@ registerLocalMediaAdapter();
const schema = em({ const schema = em({
todos: entity("todos", { todos: entity("todos", {
title: text(), title: text(),
done: boolean() done: boolean(),
}) }),
}); });
// register your schema to get automatic type completion // register your schema to get automatic type completion
@@ -27,7 +27,7 @@ declare module "bknd/core" {
export const ALL = serve<APIContext>({ export const ALL = serve<APIContext>({
// we can use any libsql config, and if omitted, uses in-memory // we can use any libsql config, and if omitted, uses in-memory
connection: { connection: {
url: "file:data.db" url: "file:data.db",
}, },
// an initial config is only applied if the database is empty // an initial config is only applied if the database is empty
initialConfig: { initialConfig: {
@@ -36,8 +36,9 @@ export const ALL = serve<APIContext>({
auth: { auth: {
enabled: true, enabled: true,
jwt: { jwt: {
secret: secureRandomString(64) issuer: "bknd-astro-example",
} secret: secureRandomString(64),
},
}, },
// ... and media // ... and media
media: { media: {
@@ -45,19 +46,19 @@ export const ALL = serve<APIContext>({
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: "./public" path: "./public",
} },
} },
} },
}, },
options: { options: {
// the seed option is only executed if the database was empty // the seed option is only executed if the database was empty
seed: async (ctx) => { seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([ await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true }, { title: "Learn bknd", done: true },
{ title: "Build something cool", done: false } { title: "Build something cool", done: false },
]); ]);
} },
}, },
// here we can hook into the app lifecycle events ... // here we can hook into the app lifecycle events ...
beforeBuild: async (app) => { beforeBuild: async (app) => {
@@ -66,11 +67,11 @@ export const ALL = serve<APIContext>({
async () => { async () => {
// ... to create an initial user // ... to create an initial user
await app.module.auth.createUser({ await app.module.auth.createUser({
email: "ds@bknd.io", email: "test@bknd.io",
password: "12345678" password: "12345678",
}); });
}, },
"sync" "sync",
); );
} },
}); });

View File

@@ -0,0 +1,36 @@
"use client";
import Image from "next/image";
import { usePathname } from "next/navigation";
export function Footer() {
const pathname = usePathname();
return (
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href={pathname === "/" ? "/ssr" : "/"}
>
<Image aria-hidden src="/file.svg" alt="File icon" width={16} height={16} />
{pathname === "/" ? "SSR" : "Home"}
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/admin"
>
<Image aria-hidden src="/window.svg" alt="Window icon" width={16} height={16} />
Admin
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://bknd.io"
target="_blank"
rel="noopener noreferrer"
>
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
Go to bknd.io
</a>
</footer>
);
}

View File

@@ -0,0 +1,76 @@
import Image from "next/image";
import type { ReactNode } from "react";
import { Footer } from "./Footer";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div className="flex flex-row items-center ">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<div className="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<Image
className="dark:invert"
src="/bknd.svg"
alt="bknd logo"
width={183}
height={59}
priority
/>
</div>
{children}
</main>
<Footer />
</div>
);
}
export const Buttons = () => (
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://docs.bknd.io/integration/nextjs"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
);
export const List = ({ items = [] }: { items: ReactNode[] }) => (
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
{items.map((item, i) => (
<li key={i} className={i < items.length - 1 ? "mb-2" : ""}>
{item}
</li>
))}
</ol>
);

View File

@@ -0,0 +1,91 @@
import { getApi } from "@/bknd";
import { revalidatePath } from "next/cache";
import { Fragment } from "react";
import { List } from "@/app/(main)/layout";
export default async function Home() {
// without "{ verify: true }", this page can be static
const api = await getApi();
const limit = 5;
const todos = await api.data.readMany("todos", { limit, sort: "-id" });
const total = todos.body.meta.total;
return (
<>
<Description />
<div className="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
<h2 className="font-mono mb-1 opacity-70">
<code>What's next?</code>
</h2>
<div className="flex flex-col w-full gap-2">
{total > limit && (
<div className="bg-foreground/10 flex justify-center p-1 text-xs rounded text-foreground/40">
{total - limit} more todo(s) hidden
</div>
)}
<div className="flex flex-col gap-3">
{todos.reverse().map((todo) => (
<div className="flex flex-row" key={String(todo.id)}>
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
<input
type="checkbox"
className="flex-shrink-0 cursor-pointer"
defaultChecked={!!todo.done}
onChange={async () => {
"use server";
const api = await getApi();
await api.data.updateOne("todos", todo.id, {
done: !todo.done,
});
revalidatePath("/");
}}
/>
<div className="text-foreground/90 leading-none">{todo.title}</div>
</div>
<button
type="button"
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
onClick={async () => {
"use server";
const api = await getApi();
await api.data.deleteOne("todos", todo.id);
revalidatePath("/");
}}
>
</button>
</div>
))}
</div>
<form
className="flex flex-row w-full gap-3 mt-2"
key={todos.map((t) => t.id).join()}
action={async (formData: FormData) => {
"use server";
const title = formData.get("title") as string;
const api = await getApi();
await api.data.createOne("todos", { title });
revalidatePath("/");
}}
>
<input
type="text"
name="title"
placeholder="New todo"
className="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
/>
<button type="submit" className="cursor-pointer">
Add
</button>
</form>
</div>
</div>
</>
);
}
const Description = () => (
<List
items={["Get started with a full backend.", "Focus on what matters instead of repetition."]}
/>
);

View File

@@ -0,0 +1,33 @@
import { getApi } from "@/bknd";
import { Buttons, List } from "../layout";
export default async function SSRPage() {
const api = await getApi({ verify: true });
const { data } = await api.data.readMany("todos");
const user = api.getUser();
return (
<>
<List items={data.map((todo) => todo.title)} />
<Buttons />
<p>
{user ? (
<>
Logged in as {user.email}.{" "}
<a className="font-medium underline" href="/api/auth/logout">
Logout
</a>
</>
) : (
<>
Not logged in.{" "}
<a className="font-medium underline" href="/admin/auth/login">
Login
</a>
</>
)}
</p>
</>
);
}

View File

@@ -1,13 +1,16 @@
import { getApp } from "@/bknd"; import { config } from "@/bknd";
import { serve } from "bknd/adapter/nextjs";
// if you're not using a local media adapter, or file database, // since we're using the local media adapter in this example,
// you can uncomment this line to enable running bknd on edge // we can't use the edge runtime.
// export const runtime = "edge"; // export const runtime = "edge";
const handler = async (request: Request) => { const handler = serve({
const app = await getApp(); ...config,
return app.fetch(request); cleanRequest: {
}; searchParams: ["bknd"],
},
});
export const GET = handler; export const GET = handler;
export const POST = handler; export const POST = handler;

View File

@@ -1,3 +0,0 @@
export const GET = async (req: Request) => {
return Response.json(process.env);
};

View File

@@ -1,97 +0,0 @@
import Image from "next/image";
import { getApi } from "@/bknd";
export default async function Home() {
const api = await getApi();
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<div className="flex flex-row items-center ">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<div className="ml-3.5 mr-2 font-mono opacity-70">&amp;</div>
<Image
className="dark:invert"
src="/bknd.svg"
alt="bknd logo"
width={183}
height={59}
priority
/>
</div>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://docs.bknd.io/integration/nextjs"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
<pre className="text-sm">
{JSON.stringify(await api.data.readMany("posts"), null, 2)}
</pre>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/ssr"
>
<Image aria-hidden src="/file.svg" alt="File icon" width={16} height={16} />
SSR
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="/admin"
>
<Image aria-hidden src="/window.svg" alt="Window icon" width={16} height={16} />
Admin
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://bknd.io"
target="_blank"
rel="noopener noreferrer"
>
<Image aria-hidden src="/globe.svg" alt="Globe icon" width={16} height={16} />
Go to bknd.io
</a>
</footer>
</div>
);
}

View File

@@ -1,14 +0,0 @@
import { getApi } from "@/bknd";
export default async function SSRPage() {
const api = await getApi({ verify: true });
const { data } = await api.data.readMany("posts");
return (
<div>
<h1>Server-Side Rendered Page</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>
<pre>{JSON.stringify(api.getUser(), null, 2)}</pre>
</div>
);
}

View File

@@ -1,5 +1,8 @@
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs"; import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
import { App } from "bknd";
import { boolean, em, entity, text } from "bknd/data";
import { registerLocalMediaAdapter } from "bknd/adapter/node"; import { registerLocalMediaAdapter } from "bknd/adapter/node";
import { secureRandomString } from "bknd/utils";
import { headers } from "next/headers"; import { headers } from "next/headers";
// The local media adapter works well in development, and server based // The local media adapter works well in development, and server based
@@ -12,10 +15,71 @@ import { headers } from "next/headers";
// For production, it is recommended to uncomment the line below. // For production, it is recommended to uncomment the line below.
registerLocalMediaAdapter(); registerLocalMediaAdapter();
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
export const config = { export const config = {
connection: { connection: {
url: process.env.DB_URL as string, url: "file:data.db",
authToken: process.env.DB_TOKEN as string, },
// an initial config is only applied if the database is empty
initialConfig: {
data: schema.toJSON(),
// we're enabling auth ...
auth: {
enabled: true,
jwt: {
issuer: "bknd-nextjs-example",
secret: secureRandomString(64),
},
cookie: {
pathSuccess: "/ssr",
pathLoggedOut: "/ssr",
},
},
// ... and media
media: {
enabled: true,
adapter: {
type: "local",
config: {
path: "./public",
},
},
},
},
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
},
},
// here we can hook into the app lifecycle events ...
beforeBuild: async (app) => {
app.emgr.onEvent(
App.Events.AppFirstBoot,
async () => {
// ... to create an initial user
await app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
"sync",
);
}, },
} as const satisfies NextjsBkndConfig; } as const satisfies NextjsBkndConfig;

View File

@@ -10,8 +10,8 @@ registerLocalMediaAdapter();
const schema = em({ const schema = em({
todos: entity("todos", { todos: entity("todos", {
title: text(), title: text(),
done: boolean() done: boolean(),
}) }),
}); });
// register your schema to get automatic type completion // register your schema to get automatic type completion
@@ -23,7 +23,7 @@ declare module "bknd/core" {
const config = { const config = {
// we can use any libsql config, and if omitted, uses in-memory // we can use any libsql config, and if omitted, uses in-memory
connection: { connection: {
url: "file:test.db" url: "file:test.db",
}, },
// an initial config is only applied if the database is empty // an initial config is only applied if the database is empty
initialConfig: { initialConfig: {
@@ -33,8 +33,8 @@ const config = {
enabled: true, enabled: true,
jwt: { jwt: {
issuer: "bknd-remix-example", issuer: "bknd-remix-example",
secret: secureRandomString(64) secret: secureRandomString(64),
} },
}, },
// ... and media // ... and media
media: { media: {
@@ -42,19 +42,19 @@ const config = {
adapter: { adapter: {
type: "local", type: "local",
config: { config: {
path: "./public" path: "./public",
} },
} },
} },
}, },
options: { options: {
// the seed option is only executed if the database was empty // the seed option is only executed if the database was empty
seed: async (ctx) => { seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([ await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true }, { title: "Learn bknd", done: true },
{ title: "Build something cool", done: false } { title: "Build something cool", done: false },
]); ]);
} },
}, },
// here we can hook into the app lifecycle events ... // here we can hook into the app lifecycle events ...
beforeBuild: async (app) => { beforeBuild: async (app) => {
@@ -63,13 +63,13 @@ const config = {
async () => { async () => {
// ... to create an initial user // ... to create an initial user
await app.module.auth.createUser({ await app.module.auth.createUser({
email: "ds@bknd.io", email: "test@bknd.io",
password: "12345678" password: "12345678",
}); });
}, },
"sync" "sync",
); );
} },
} as const satisfies RemixBkndConfig; } as const satisfies RemixBkndConfig;
export async function getApp(args?: { request: Request }) { export async function getApp(args?: { request: Request }) {