# Better Auth quick start

> Create a user, register a personal endpoint through authenticated routes, and list it. One file, SQLite, no PostgreSQL.

Source: https://good-webhooks.dev/docs/better-auth/quick-start



One script runs the plugin end to end against a SQLite file. No server, no PostgreSQL, and no webhook is sent:

What the script checks:

1. **No session**: Listing endpoints without signing in returns `401`.
2. **Create**: A signed-in user creates an endpoint and gets its secret once.
3. **List**: The endpoint is listed, without its secret.
4. **Other scope**: Asking for application scope returns `403`.

You need Node.js 24 and npm.

### Create a project [#create-a-project]

```sh
mkdir good-webhooks-auth-quickstart && cd good-webhooks-auth-quickstart
npm init -y
```

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm install good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add good-webhooks@beta better-auth@1.7.7
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Generate an auth secret for this shell session:

```sh
export BETTER_AUTH_SECRET="$(node -e 'console.log(require("node:crypto").randomBytes(32).toString("base64"))')"
```

### Write the script [#write-the-script]

Save as `quick-start.mjs`. It applies Better Auth's schema programmatically because this is a disposable SQLite database. A real application uses its [normal schema workflow](https://good-webhooks.dev/docs/better-auth.md#generate-and-apply-the-schema).

```js title="quick-start.mjs"
import assert from 'node:assert/strict'
import { randomUUID } from 'node:crypto'
import { DatabaseSync } from 'node:sqlite'
import { betterAuth } from 'better-auth'
import { getMigrations } from 'better-auth/db/migration'
import { goodWebhooks } from 'good-webhooks/better-auth'

const origin = 'http://localhost:3000'
const options = {
  database: new DatabaseSync('./quick-start.sqlite'),
  baseURL: origin,
  secret: process.env.BETTER_AUTH_SECRET,
  emailAndPassword: { enabled: true },
  plugins: [goodWebhooks({ eventTypes: ['invoice.created', 'invoice.paid'] })],
}

await (await getMigrations(options)).runMigrations()
const auth = betterAuth(options)

// Call a plugin route the way a browser would, with a JSON body and cookies.
async function call(operation, body = {}, cookie = '') {
  return auth.handler(
    new Request(`${origin}/api/auth/good-webhooks/${operation}`, {
      method: 'POST',
      headers: { origin, 'content-type': 'application/json', cookie },
      body: JSON.stringify(body),
    }),
  )
}

// Sign up and keep the session cookie.
const signup = await auth.api.signUpEmail({
  body: { name: 'Quick start', email: `${randomUUID()}@example.com`, password: randomUUID() },
  asResponse: true,
})
const cookie = signup.headers
  .getSetCookie()
  .map((value) => value.split(';')[0])
  .join('; ')

// No session: rejected.
assert.equal((await call('list')).status, 401)
console.log('Anonymous list rejected with 401')

// Create a personal endpoint.
const created = await (
  await call(
    'create',
    { url: 'https://receiver.example/webhooks', eventTypes: ['invoice.created'] },
    cookie,
  )
).json()
assert.match(created.secret, /^whsec_/)
console.log('Created endpoint', created.endpoint.id, 'with a one-time secret')

// List it. Secrets never appear in reads.
const endpoints = await (await call('list', {}, cookie)).json()
assert.equal(endpoints.length, 1)
assert.equal('secret' in endpoints[0], false)
console.log('Listed 1 endpoint without its secret')

// Application scope needs an explicit policy.
assert.equal((await call('list', { scope: null }, cookie)).status, 403)
console.log('Application scope rejected with 403')

await call('remove', { id: created.endpoint.id }, cookie)
options.database.close()
```

### Run it [#run-it]

```sh
node quick-start.mjs
```

```text
Anonymous list rejected with 401
Created endpoint <id> with a one-time secret
Listed 1 endpoint without its secret
Application scope rejected with 403
```

`auth.handler()` receives real `Request` objects, so there is no HTTP server to start. On repeated runs Better Auth's SQLite inspector may warn that array fields use `TEXT` storage. That is expected.

### Clean up [#clean-up]

Delete `quick-start.sqlite` when you are done.

## Next steps [#next-steps]

- [Add the plugin to your app](https://good-webhooks.dev/docs/better-auth.md): Server plugin, schema, and client plugin in an existing auth instance.
- [Permissions](https://good-webhooks.dev/docs/better-auth/permissions.md): Organization roles and custom scope policies.
- [Add delivery](https://good-webhooks.dev/docs/better-auth/delivery.md): Send the events these endpoints subscribe to.
