Understand the boundary
ZeeDB has no secondary-index or uniqueness-constraint API. You can store an application index in another collection, but your application owns validation, concurrency coordination, migration, and repair.
For an email lookup, a possible model is:
| Collection | Key | Value |
|---|---|---|
users | User ID | Encoded user document |
users_by_email | Application-canonical email | User ID bytes |
Choose a canonicalization policy appropriate for your application. ZeeDB will not normalize the key for you.
Coordinate the logical operation
A uniqueness check and a write are separate API calls. Protect their whole sequence with an application mutex and ensure every mutation path for that invariant uses it.
mu.Lock()
defer mu.Unlock()
_, err := db.Get(ctx, "users_by_email", []byte(email))
if err == nil {
return errors.New("email already in use")
}
if !errors.Is(err, zeedb.ErrNotFound) {
return err
}
This sketch assumes a new primary key has also been checked under the same lock, inputs have been validated, and encodedUser has already been produced. It is not an SDK transaction callback.
Publish both records together
err = db.WriteBatch(ctx, []zeedb.Mutation{
{Collection: "users", Key: []byte(userID), Value: encodedUser},
{Collection: "users_by_email", Key: []byte(email), Value: []byte(userID)},
})
The batch makes the two resulting mutations visible atomically. The application mutex coordinates the earlier uniqueness check. Neither property replaces the other.
Hide the raw DB behind your repository if other callers might bypass the mutex. The process-level file lock does not coordinate goroutine-level application invariants.
Updates and deletes
An email change must read/validate the previous user, verify ownership of index entries, reject a conflicting new email, remove the old mapping, and write the replacement mapping with primary data in one batch. Hold the same application lock throughout.
Deleting a user should delete its owned index mapping in the same batch. A generic deletion of a guessed mapping can remove another user’s entry if ownership is not checked.
Failures and reconciliation
A validation failure leaves the batch unapplied. A write/checkpoint I/O failure is indeterminate: close/reopen and inspect both records before retrying. Do not interpret an error as proof that uniqueness was never reserved.
For repair or a migration, quiesce writers and check primary/index relationships using bounded scans. Several pages do not form one stable snapshot while other goroutines write. Plan restartable progress and make a closed-directory backup first.