> For the complete documentation index, see [llms.txt](https://brightercommand.gitbook.io/paramore-brighter-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://brightercommand.gitbook.io/paramore-brighter-documentation/outbox-and-inbox/brighterinboxsupport/spannerinbox.md).

# Spanner Inbox

The Spanner Inbox records the messages a consumer has already handled in a Google Cloud Spanner table, so a redelivery is recognised rather than reprocessed.

> **Reference** · Applies to **Brighter V10** · Prerequisites: [Inbox Support](/paramore-brighter-documentation/outbox-and-inbox/brighterinboxsupport.md)

The Spanner Inbox records the messages a consumer has already handled in a Google Cloud Spanner table, so a redelivery is recognised rather than reprocessed. It is configured entirely through the shared relational configuration.

## Spanner Inbox Configuration

```powershell
dotnet add package Paramore.Brighter.Inbox.Spanner
dotnet add package Paramore.Brighter.Spanner
```

**The class is `SpannerInboxAsync`, and there is no synchronous twin.** It implements Brighter's inbox interfaces through `RelationalDatabaseInbox`, so it is used exactly like any other Inbox; only the name is different.

```csharp
using Microsoft.Extensions.DependencyInjection;
using Paramore.Brighter;
using Paramore.Brighter.Inbox;
using Paramore.Brighter.Inbox.Spanner;
using Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection;

public static class SpannerInboxRegistration
{
    public static void ConfigureServices(IServiceCollection services)
    {
        var configuration = new RelationalDatabaseConfiguration(
            connectionString: "Data Source=projects/my-project/instances/my-instance/databases/brighter",
            inboxTableName: "Inbox");

        services.AddSingleton<IAmARelationalDatabaseConfiguration>(configuration);

        services.AddConsumers(options =>
        {
            options.InboxConfiguration = new InboxConfiguration(
                new SpannerInboxAsync(configuration),
                scope: InboxScope.Commands,
                onceOnly: true,
                actionOnExists: OnceOnlyAction.Warn);
            // ... your subscriptions and channel factory
        });
    }
}
```

Before Brighter 10.8.0 this configuration took effect only in an application that also called `AddProducers`; see [Global Inbox Configuration in a Consumer-Only Application](/paramore-brighter-documentation/outbox-and-inbox/brighterinboxsupport.md#global-inbox-configuration-in-a-consumer-only-application).

Constructed with the configuration alone, `SpannerInboxAsync` builds its own `SpannerConnectionProvider`; a second constructor takes an `IAmARelationalDbConnectionProvider` when you want to share one.

## Spanner Inbox Options

**The Spanner Inbox has no options type of its own.** It takes an `IAmARelationalDatabaseConfiguration`, the type every relational Outbox, Inbox, provisioner and queue-table transport takes, and its options are documented once at [Relational Database Configuration Reference](/paramore-brighter-documentation/brighter-configuration/brighterbasicconfiguration/relationaldatabaseconfigurationreference.md#relational-database-configuration-options). That is why this page has no table: there is no Spanner Inbox surface to tabulate.

The option that matters here is **`inboxTableName`**, which defaults to `Inbox`. Note the asymmetry the reference page records — the constructor parameter is `inboxTableName` and the property you read back is `InBoxTableName`, with a capital `B`.

`schemaName` is not used: Spanner does not organise tables into schemas the way the other relational backends do, and its provisioning extensions take no `schemaName` argument.

## Provisioning the Spanner Inbox

You have the same two options every relational Inbox has.

**Option A — let Brighter provision it.** `AddSpannerInbox` registers a provisioner that creates the table at startup; see [Configuring Box Provisioning](/paramore-brighter-documentation/outbox-and-inbox/boxprovisioning/boxprovisioningconfiguration.md#spanner). Spanner uses the **degenerate runner** — it creates the table on a fresh database and cannot evolve an existing one, so an Inbox table created by an earlier Brighter release needs its new columns added by hand. See [Per-backend differences to be aware of](/paramore-brighter-documentation/outbox-and-inbox/boxprovisioning.md#per-backend-differences-to-be-aware-of), and [Replay On Seen Reference](/paramore-brighter-documentation/outbox-and-inbox/replayonseen/replayonseenreference.md) for the causation column that [replay on seen](/paramore-brighter-documentation/outbox-and-inbox/replayonseen.md) needs on both boxes.

**Option B — manage the DDL yourself.** `SpannerInboxBuilder.GetDDL()` returns the DDL Brighter ships:

```csharp
using Paramore.Brighter.Inbox.Spanner;

public static class SpannerInboxDdl
{
    public static string Ddl() => SpannerInboxBuilder.GetDDL("Inbox");
}
```

The table is keyed on `(CommandId, ContextKey)`, so the same command handled by two different handlers is two rows rather than a collision:

```sql
CREATE TABLE IF NOT EXISTS `Inbox`(
    `CommandId` STRING(256) NOT NULL,
    `CommandType` STRING(256),
    `CommandBody` JSON,
    `Timestamp` TIMESTAMP,
    `ContextKey` STRING(256),
    `CausationId` STRING(256)
) PRIMARY KEY (`CommandId`, `ContextKey`)
```

`CausationId` arrives at Brighter 10.8.0. A table an earlier release created still records commands without it, but [Replay On Seen](/paramore-brighter-documentation/outbox-and-inbox/replayonseen/replayonseenreference.md#store-support) needs it, and the Spanner provisioner adds it only on a fresh install, so add it by hand to an existing table.

Duplicate inserts surface as a `SpannerException` carrying an `AlreadyExists` status, which the Inbox translates into the `OnceOnlyAction` you configured.

## Further Reading

* [Inbox Support](/paramore-brighter-documentation/outbox-and-inbox/brighterinboxsupport.md) — what an Inbox is for and when you need one
* [Relational Database Configuration Reference](/paramore-brighter-documentation/brighter-configuration/brighterbasicconfiguration/relationaldatabaseconfigurationreference.md) — every option this Inbox takes
* [Spanner Outbox](/paramore-brighter-documentation/outbox-and-inbox/brighteroutboxsupport/spanneroutbox.md) — the same configuration type, one family over
* [Database Provisioning](/paramore-brighter-documentation/outbox-and-inbox/boxprovisioning.md) — the migration machinery, and why Spanner's runner is degenerate
* [Dispatcher Configuration Reference](/paramore-brighter-documentation/brighter-configuration/brighterbasicconfiguration/dispatcherconfigurationreference.md#inbox) — where `InboxConfiguration` fits


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://brightercommand.gitbook.io/paramore-brighter-documentation/outbox-and-inbox/brighterinboxsupport/spannerinbox.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
