---
title: Protect your orders
description: Add email and password sign-in, bind new orders to the session, and keep each customer's orders private.
sidebar:
  icon: shield-check
---

This chapter extends the same SQLite-backed `relkit-orders` app. You will add a local Better Auth
service, then require a session for both Orders routes. A caller cannot choose an order owner in
request JSON; the server gets that identity from the session. Stop `bun run dev` before migrating.

1. ## 1. Add authentication to the existing database [#add-authentication-to-the-existing-database]

    The auth scaffold installs Better Auth and creates its service and `/api/auth` route. It reuses
    the database service already in the app. It does not apply a migration or create a secret.

    ```sh title="Terminal — relkit-orders"
    bun run relkit add auth --adapter better-auth --database-dialect sqlite
    ```

    This includes the native SDK, development declaration peers and selected repairs. If you pass
    `--no-install`, finish with `bun install` and `bun run check` before continuing. The
    [manual auth setup](/docs/modules/auth/better-auth) is an alternative, not another required installation.

    Application HTTP fixtures must now import `createTestApplication` from
    `@relkit/testing/better-auth`, including the existing Hello test. The native helper activates both
    auth and its database; see [the complete updated fixtures](/docs/learn/test-orders).

    **Extend** `src/services/database/schema/index.ts` with the auth tables. The complete source below
    retains `orders` and adds the `user`, `session`, `account`, and `verification` exports Better Auth
    requires. Do not replace an existing business table when adapting this to your own application.

    ```ts title="src/services/database/schema/index.ts"
    /** Registers tutorial order ownership columns alongside native Better Auth user/session/account/verification tables. */
    import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";

    export const orders = sqliteTable("orders", {
      orderId: text().primaryKey(),
      sku: text().notNull(),
      quantity: integer().notNull(),
      totalCents: integer().notNull(),
      ownerId: text(),
    });

    const timestamps = {
      createdAt: integer({ mode: "timestamp" }).notNull(),
      updatedAt: integer({ mode: "timestamp" }).notNull(),
    };
    export const user = sqliteTable("user", {
      id: text().primaryKey(),
      name: text().notNull(),
      email: text().notNull().unique(),
      emailVerified: integer({ mode: "boolean" }).notNull(),
      image: text(),
      ...timestamps,
    });

    export const session = sqliteTable("session", {
      id: text().primaryKey(),
      expiresAt: integer({ mode: "timestamp" }).notNull(),
      token: text().notNull().unique(),
      ipAddress: text(),
      userAgent: text(),
      userId: text().notNull(),
      ...timestamps,
    });

    export const account = sqliteTable(
      "account",
      {
        id: text().primaryKey(),
        accountId: text().notNull(),
        issuer: text().notNull(),
        providerId: text().notNull(),
        userId: text().notNull(),
        accessToken: text(),
        refreshToken: text(),
        idToken: text(),
        accessTokenExpiresAt: integer({ mode: "timestamp" }),
        refreshTokenExpiresAt: integer({ mode: "timestamp" }),
        scope: text(),
        password: text(),
        ...timestamps,
      },
      (table) => [uniqueIndex("account_issuer_account_id").on(table.issuer, table.accountId)],
    );

    export const verification = sqliteTable("verification", {
      id: text().primaryKey(),
      identifier: text().notNull(),
      value: text().notNull(),
      expiresAt: integer({ mode: "timestamp" }).notNull(),
      ...timestamps,
    });
    ```

    Replace `src/platform/env.ts` with the existing fields plus the auth secret and base URL:

    ```ts title="src/platform/env.ts"
    import { defineEnv, env } from "@relkit/app/config";

    export default defineEnv({
      LOG_LEVEL: env.literal("trace", "debug", "info", "warn", "error").default("info"),
      DATABASE_PATH: env.string().default("./orders.sqlite"),
      BETTER_AUTH_SECRET: env.secret(),
      BETTER_AUTH_URL: env.url().default(new URL("http://127.0.0.1:3000")),
    });
    ```

    Set a stable local secret in the terminal that starts the app, then append it to the ignored
    `.env` for later restarts. Never commit that file.

    ```sh title="Terminal — relkit-orders"
    export DATABASE_PATH=./orders.sqlite
    export BETTER_AUTH_URL=http://127.0.0.1:3000
    export BETTER_AUTH_SECRET="$(openssl rand -base64 32)"
    printf 'BETTER_AUTH_URL=%s\nBETTER_AUTH_SECRET=%s\n' \
      "$BETTER_AUTH_URL" "$BETTER_AUTH_SECRET" >> .env
    ```

    Review `src/services/auth/service.ts` and make it match the email/password configuration below:

    ```ts title="src/services/auth/service.ts"
    /** Declares email/password Better Auth using the configured URL and the sole database owner's native tables. */
    import { defineBetterAuthService } from "@relkit/better-auth";

    export default defineBetterAuthService({
      baseURL: process.env.BETTER_AUTH_URL ?? "http://127.0.0.1:3000",
      emailAndPassword: { enabled: true },
    });
    ```

    The auth mount handles `/api/auth` and rejects signed-out calls to both `/orders` and
    `/orders/*`. Replace the scaffolded route with:

    ```ts title="src/routes/api/auth/[[...auth]]/route.ts"
    /** Mounts native Better Auth once for all methods and protects both root and descendant order routes. */
    import { defineRoute } from "@relkit/app/routes";
    import auth from "@app/services/auth/service.js";

    export const ALL = defineRoute({
      handler: auth.handler,
      auth: { protected: ["/orders", "/orders/*"] },
    });
    ```

    Generate a second migration and inspect its SQL before applying it. This keeps the existing
    Orders table and adds auth tables; do not regenerate the initial migration.

    ```sh title="Terminal — relkit-orders"
    bun --bun drizzle-kit generate --name=auth
    bun run relkit db migrate
    bun run check
    ```

2. ## 2. Associate orders with the signed-in user [#associate-orders-with-the-signed-in-user]

    Replace the two Orders functions below. `createOrder` obtains the user ID from the session
    and stores it with the row. `getOrder` matches both `orderId` and the session's user ID;
    someone else's order returns `found: false`.

    ```ts title="src/services/orders/functions/create-order.function.ts"
    /** Prices validated order input and persists it with the authenticated session user's ownership. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";
    import priceOrder from "@app/services/orders/functions/price-order.function.js";
    const order = z.object({
      orderId: z.string().min(1),
      sku: z.string().min(1),
      quantity: z.number().int().positive(),
    });
    export default defineFunction({
      input: order,
      output: z.object({
        orderId: z.string(),
        sku: z.string(),
        quantity: z.number().int().positive(),
        totalCents: z.number().int().nonnegative(),
      }),
      handler: async (input, context) => {
        const session = await context.auth.getSession();
        if (session === null) throw new Error("Authentication required");
        const { totalCents } = await priceOrder.invoke({ quantity: input.quantity });
        const saved = await context.database.orders.insert({
          data: { ...input, totalCents, ownerId: session.user.id },
        });
        return {
          orderId: saved.orderId,
          sku: saved.sku,
          quantity: saved.quantity,
          totalCents: saved.totalCents,
        };
      },
    });
    ```

    ```ts title="src/services/orders/functions/get-order.function.ts"
    /** Reads a validated order ID only within the authenticated user's ownership and returns a concealed missing result otherwise. */
    import { defineFunction } from "@relkit/app/functions";
    import { z } from "@relkit/app/schema";
    export default defineFunction({
      input: z.object({ orderId: z.string().min(1) }),
      output: z.object({
        found: z.boolean(),
        order: z
          .object({
            orderId: z.string(),
            sku: z.string(),
            quantity: z.number(),
            totalCents: z.number(),
          })
          .optional(),
      }),
      handler: async ({ orderId }, context) => {
        const session = await context.auth.getSession();
        if (session === null) throw new Error("Authentication required");
        const row = await context.database.orders.findOne({
          where: { orderId, ownerId: session.user.id },
        });
        return row === null
          ? { found: false }
          : {
              found: true,
              order: {
                orderId: row.orderId,
                sku: row.sku,
                quantity: row.quantity,
                totalCents: row.totalCents,
              },
            };
      },
    });
    ```

    ```text
    request cookie → session → user ID → Orders query → owner-matching row
    ```

    Rows created in the previous chapter have no owner. These protected reads do not reveal them to
    new accounts; this tutorial does not guess who owned an anonymous order.

3. ## 3. Sign in and verify access [#sign-in-and-verify-access]

    Run `bun run typecheck` and `bun run dev` in the terminal with the auth environment set. In
    another terminal, first call `GET /orders/saved-1` without a cookie; expect HTTP `401`.
    Then create a local account and save its cookie:

    ```sh title="Second terminal — relkit-orders"
    auth_cookies=$(mktemp -t relkit-orders-cookies)
    curl -i -c "$auth_cookies" http://127.0.0.1:3000/api/auth/sign-up/email \
      -H 'content-type: application/json' -H 'origin: http://127.0.0.1:3000' \
      --data '{"name":"Demo","email":"demo@example.com","password":"local-demo-password-123"}'
    curl -i -b "$auth_cookies" -X POST http://127.0.0.1:3000/orders \
      -H 'content-type: application/json' \
      -d '{"orderId":"private-1","sku":"book","quantity":3}'
    curl -b "$auth_cookies" http://127.0.0.1:3000/orders/private-1
    ```

    Expect sign-up and POST success, followed by `found: true`. A different signed-in account
    receives `found: false` for `private-1`.

{/* Generated by apps/docs/scripts/generate.ts. Do not edit. */}

**Reference and source**

- [`@relkit/better-auth` API](/docs/api/better-auth)
- [`@relkit/drizzle` API](/docs/api/drizzle)

- [`examples/auth-drizzle/src/services/auth/service.ts`](https://github.com/rel-kit/relkit/blob/main/examples/auth-drizzle/src/services/auth/service.ts)
