# Server SDK

> @findlane/server uploads and updates catalogs, changes settings, and searches with a workspace API key. It runs in Node.js 18+, Bun, Deno, and edge runtimes.

## Install

```sh
npm install @findlane/server
```

## Create a client

```ts
import { SearchServerClient } from "@findlane/server"

const client = new SearchServerClient({
  workspaceId: process.env.FINDLANE_WORKSPACE_ID!,
  apiKey: process.env.FINDLANE_API_KEY!,
})

const products = client.index<Product>("products")
```

| Option        |                                                               |
| ------------- | ------------------------------------------------------------- |
| `workspaceId` | The workspace ID, from the **API keys** or **Settings** page. |
| `apiKey`      | A workspace API key. Keep it on your server.                  |
| `baseUrl`     | The API to call. Leave it out to use Findlane's hosted API.   |
| `fetch`       | A `fetch` implementation to use instead of the global one.    |

`client.index<T>(name)` returns the methods for one index. `T` types your records, which each need a string `objectID`. Every method returns a promise and takes an optional `AbortSignal`.

## Search

| Method                     | Scope    |                                                                                                                |
| -------------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `search(request, signal?)` | `search` | Search the index. Takes the [same request](/docs/browser-sdk.md#request) as the browser SDK, with up to 100 hits. |

## Records

| Method                                | Scope   |                                                                   |
| ------------------------------------- | ------- | ----------------------------------------------------------------- |
| `upsert(records, signal?)`            | `write` | Add or replace up to 1,000 records by `objectID`.                 |
| `delete(objectID, signal?)`           | `write` | Delete a record.                                                  |
| `getRecord(objectID, signal?)`        | either  | A record and its version; `record` is `null` if it doesn't exist. |
| `upsertOne(record, options)`          | `write` | Write one record if it's still at `options.expectedVersion`.      |
| `deleteOne(objectID, options)`        | `write` | Delete one record if it's still at `options.expectedVersion`.     |
| `replaceAll(records, signal?)`        | `write` | Replace the catalog in one request, up to 100,000 records.        |
| `replaceAllStaged(records, options?)` | `write` | Replace the catalog in batches, switched in one commit.           |
| `abortReplacement(stageId, signal?)`  | `write` | Drop an unfinished staged replacement.                            |

Writes return a receipt: `{ acceptedRevision, indexedRevision, recordCount }`. See [Records](/docs/records.md) for how to use them.

`replaceAllStaged` takes an array or an async iterable of records, and these options:

| Option          |                                                                 |
| --------------- | --------------------------------------------------------------- |
| `stageId`       | Names the stage, so a retry continues it. Default: a random ID. |
| `batchSize`     | Records per batch, 1 to 1,000. Default 500.                     |
| `maxBatchBytes` | Bytes per batch. Default 8 MiB.                                 |
| `signal`        | Cancels the upload.                                             |

`upsertOne` and `deleteOne` take `{ expectedVersion, idempotencyKey?, signal? }`. The lower-level `beginReplacement`, `uploadReplacementBatch`, and `commitReplacement` are also available for running the stage steps yourself.

## Settings

| Method                               | Scope   |                                                                        |
| ------------------------------------ | ------- | ---------------------------------------------------------------------- |
| `getSettings(signal?)`               | either  | The index's settings, and a settings change in progress in `rebuild`.  |
| `replaceSettings(settings, signal?)` | `write` | Save the full settings. `applied` says how they took effect.           |
| `waitForSettings(options?)`          | either  | Wait until a background settings change finishes; throws if it failed. |

See [Search settings](/docs/settings.md) for the fields and how changes apply.

## Indexing status

| Method                                 | Scope  |                                          |
| -------------------------------------- | ------ | ---------------------------------------- |
| `getIndexingStatus(signal?)`           | either | `{ acceptedRevision, indexedRevision }`. |
| `waitUntilIndexed(revision, options?)` | either | Wait until a revision is searchable.     |

Writes are searchable when they return, so `waitUntilIndexed` returns at once. It's there so code written for search services that index later keeps working.

## Browser search access

| Method                                   | Scope   |                                               |
| ---------------------------------------- | ------- | --------------------------------------------- |
| `getPublicSearchAccess(signal?)`         | `write` | The public ID, allowed origins, and policy.   |
| `setPublicSearchPolicy(policy, signal?)` | `write` | Limit what browser search can read and query. |
| `usePublicIndexDefaults(signal?)`        | `write` | Remove the policy.                            |

Allowed origins are changed in the dashboard. See [Security](/docs/security.md#what-browser-search-can-read).

## Errors

A failed request throws a `FindlaneError` with the HTTP `status` and the API's `message`. `replaceAllStaged` throws a `StagedReplacementError` with the `stageId` to retry or abort, and the original error as its `cause`.

```ts
import { FindlaneError } from "@findlane/server"

try {
  await products.upsert(records)
} catch (error) {
  if (error instanceof FindlaneError && error.status === 402) alertTeam(error.message)
  else throw error
}
```

The staged replacement and conditional write methods retry network errors and `408`, `429`, `502`, `503`, and `504` responses up to three times. See the [error codes](/docs/api.md#errors).
