> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gocopper.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactions

In Copper, transactions travel through `context.Context`. When a transaction is active, every `Get`, `Select`, and `Exec` that receives that context automatically runs inside it. Your queries and services do not need a `tx` parameter, and the same code works identically inside and outside a transaction.

### Automatic Transactions per Request

Apps scaffolded with a database include `csql.TxMiddleware` as a global middleware. It wraps every **mutating** request (POST, PUT, PATCH, DELETE) in a transaction:

* On a 2xx or 3xx response, the transaction commits.
* On a 4xx or 5xx response, it rolls back.
* GET, HEAD, and OPTIONS requests are not wrapped; reads auto-commit against the pool.

As a result, a handler that writes three rows and then fails is atomic by default, without any transaction plumbing in your handlers.

<Note>
  If the commit itself fails on a 2xx response, the middleware will respond with a 500 instead, so a client never sees a success status for data that was not committed.
</Note>

### Explicit Transactions

Outside of HTTP handlers, such as background workers or CLI tasks, you may manage transactions explicitly using `InTx`:

```go theme={null}
err := s.querier.InTx(ctx, func(ctx context.Context) error {
	if err := s.queries.DeductFuel(ctx, rocketID, amount); err != nil {
		return err
	}

	return s.queries.RecordBurn(ctx, rocketID, amount)
})
```

Return `nil` to commit, or an error to roll everything back. Panics are caught by a deferred rollback, so a transaction is never left open.

`InReadOnlyTx` works the same way but always rolls back, which is useful for running multiple reads against one consistent snapshot.

### Running After the Commit

Some side effects, such as publishing an event or sending an email, should only happen if the transaction commits. You may register them using `OnCommit`:

```go theme={null}
func (s *Svc) LaunchRocket(ctx context.Context, id int64) error {
	launch, err := s.queries.InsertLaunch(ctx, id)
	if err != nil {
		return err
	}

	event := LaunchedEvent{LaunchID: launch.ID, RocketID: id}

	return s.querier.OnCommit(ctx, func(ctx context.Context) error {
		return s.pubsub.Publish(ctx, TopicRocketLaunched, event)
	})
}
```

If the transaction rolls back, the callback is discarded. Since callbacks run in the background with a 30-second timeout, you should snapshot the data you need (like `event` above) before registering the callback rather than re-reading it inside.

<Note>
  If there is no transaction in the context, `OnCommit` runs the callback immediately in the background, so the same service code works with or without an ambient transaction.
</Note>

### Escaping the Transaction

Occasionally a write should persist even if the surrounding transaction rolls back, such as an audit log or a failure record. You may use `csql.CtxWithoutTx` to run a query outside the ambient transaction:

```go theme={null}
// Recorded even if the request's transaction rolls back
_ = s.queries.RecordLaunchAttempt(csql.CtxWithoutTx(ctx), rocketID)
```

All other context values, such as deadlines, tracing, and auth, are preserved; only the transaction is removed.

### Dry Runs

Because the request transaction commits or rolls back based on your response, you get dry-run support almost for free: do the real work, then roll back before responding.

```go theme={null}
func (ro *Router) HandleLaunchRocket(w http.ResponseWriter, r *http.Request) {
	var (
		ctx = r.Context()
		id  = chttp.URLParams(r)["id"]
	)

	launch, err := ro.rockets.Launch(ctx, id)
	if err != nil {
		ro.logger.Error("Failed to launch rocket", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	if r.URL.Query().Get("dry_run") == "true" {
		_ = ro.querier.RollbackTx(ctx)
	}

	ro.rw.WriteJSON(w, chttp.WriteJSONParams{Data: launch})
}
```

The full code path runs, including validations, queries, and computed results, but nothing is persisted, and any `OnCommit` callbacks are discarded with the rollback.

### Next

Next, learn how to define and evolve your schema with [Migrations](/database/migrations).
