The StitchDB C# client for .NET, MAUI and Unity: typed models with LINQ, auth, storage, realtime, Edge Functions, errors and the app services: push, purchases, flags, jobs, metering, SQL and more.
#Install
The SDK targets .NET Standard 2.1: .NET Core 3.0+, .NET 5–10, MAUI, Xamarin and Unity. Add it to your app from its GitHub source:
git clone https://github.com/stitchdb/stitchdb-csharp
dotnet add reference stitchdb-csharp/packages/StitchDB/StitchDB/StitchDB.csprojor in your .csproj:
<ItemGroup>
<ProjectReference Include="../stitchdb-csharp/packages/StitchDB/StitchDB/StitchDB.csproj" />
</ItemGroup>The StitchDB project brings Auth (StitchDB.Gotrue), the database (StitchDB.Postgrest), Storage, Realtime and Functions with it. Each one can also be referenced on its own from packages/.
#Create a client
using StitchDB;
var url = Environment.GetEnvironmentVariable("STITCHDB_URL"); // https://<project-ref>.p.stitchdb.com
var key = Environment.GetEnvironmentVariable("STITCHDB_KEY");
var stitchdb = new StitchDB.Client(url, key, new StitchDBOptions
{
AutoRefreshToken = true,
AutoConnectRealtime = true,
});
await stitchdb.InitializeAsync();Create the client once and reuse it. Use a separate client with the service_role key for admin work on a server.
#Models
Tables map to classes that extend BaseModel:
using StitchDB.Postgrest.Attributes;
using StitchDB.Postgrest.Models;
[Table("todos")]
public class Todo : BaseModel
{
[PrimaryKey("id", false)] public long Id { get; set; } // false: generated by the database
[Column("task")] public string Task { get; set; } = "";
[Column("is_complete")] public bool IsComplete { get; set; }
[Column("user_id", ignoreOnInsert: true, ignoreOnUpdate: true)] public string? UserId { get; set; }
[Column("created_at", ignoreOnInsert: true)] public DateTime CreatedAt { get; set; }
}#Database
using static StitchDB.Postgrest.Constants;
// Select with filters, order and paging
var res = await stitchdb.From<Todo>()
.Where(x => x.IsComplete == false)
.Order(x => x.CreatedAt, Ordering.Descending)
.Range(0, 9)
.Get();
List<Todo> todos = res.Models;
var one = await stitchdb.From<Todo>().Where(x => x.Id == 1).Single();
var count = await stitchdb.From<Todo>().Where(x => x.IsComplete).Count(CountType.Exact);
// Insert, update, upsert, delete
var inserted = (await stitchdb.From<Todo>().Insert(new Todo { Task = "Ship it" })).Models.First();
await stitchdb.From<Todo>().Where(x => x.Id == inserted.Id).Set(x => x.IsComplete, true).Update();
inserted.Task = "Ship it today";
await inserted.Update<Todo>();
await stitchdb.From<Todo>().Upsert(new Todo { Id = 1, Task = "Ship it" });
await stitchdb.From<Todo>().Where(x => x.Id == 1).Delete();
// Filters by column name, and functions
await stitchdb.From<Todo>().Filter("task", Operator.ILike, "%ship%").Get();
var open = await stitchdb.Rpc<int>("count_open_todos", new Dictionary<string, object> { { "owner", userId } });Where takes LINQ expressions (==, !=, <, >, &&, ||); Filter, Not, Or and Match take any REST filter. Select("id, task") limits the columns.
#Auth
using StitchDB.Gotrue;
using static StitchDB.Gotrue.Constants;
var session = await stitchdb.Auth.SignUp("[email protected]", "a-strong-password");
session = await stitchdb.Auth.SignIn("[email protected]", "a-strong-password");
var user = stitchdb.Auth.CurrentUser;
// Magic link / one-time code
await stitchdb.Auth.SignInWithOtp(new SignInWithPasswordlessEmailOptions("[email protected]") { EmailRedirectTo = "https://example.com/welcome" });
await stitchdb.Auth.VerifyOTP("[email protected]", "123456", EmailOtpType.Email);
// OAuth: open state.Uri in a browser; the redirect brings the user back signed in
var state = await stitchdb.Auth.SignIn(Provider.Github, new SignInOptions { RedirectTo = "myapp://callback" });
await stitchdb.Auth.Update(new UserAttributes { Data = new() { { "display_name", "Ada" } } });
await stitchdb.Auth.ResetPasswordForEmail("[email protected]");
await stitchdb.Auth.RefreshSession();
await stitchdb.Auth.SignOut();
stitchdb.Auth.AddStateChangedListener((sender, changed) =>
{
// AuthState.SignedIn, SignedOut, TokenRefreshed, UserUpdated, ...
});Admin calls use the service_role key: stitchdb.AdminAuth(serviceRoleKey).CreateUser(new AdminUserAttributes { Email = ..., Password = ..., EmailConfirm = true }), ListUsers(), DeleteUser(id). To keep a session across app restarts, set SessionHandler in StitchDBOptions.
#Storage
var bucket = stitchdb.Storage.From("avatars");
await bucket.Upload(File.ReadAllBytes("avatar.png"), "user-1/avatar.png",
new StitchDB.Storage.FileOptions { ContentType = "image/png", Upsert = true });
byte[] bytes = await bucket.Download("user-1/avatar.png", (EventHandler<float>?)null);
string publicUrl = bucket.GetPublicUrl("user-1/avatar.png"); // public buckets
string signedUrl = await bucket.CreateSignedUrl("user-1/avatar.png", 60); // private, 60 s
var files = await bucket.List("user-1");
await bucket.Move("user-1/avatar.png", "user-1/old.png");
await bucket.Remove(new List<string> { "user-1/old.png" });
// Buckets (service_role key)
await admin.Storage.CreateBucket("avatars", new StitchDB.Storage.BucketUpsertOptions { Public = true });#Realtime
using StitchDB.Realtime.Models;
using StitchDB.Realtime.PostgresChanges;
using static StitchDB.Realtime.PostgresChanges.PostgresChangesOptions;
// Database changes
var changes = stitchdb.Realtime.Channel("todos-changes");
changes.OnPostgresChange((_, change) => Console.WriteLine(change.Model<Todo>()?.Task),
ListenType.Inserts, new PostgresChangesFilter { Schema = "public", Table = "todos" });
await changes.Subscribe();
// Broadcast (receive your own messages too)
var room = stitchdb.Realtime.Channel("room-1");
var broadcast = room.Register<BaseBroadcast>(true, true);
broadcast.AddBroadcastEventHandler((_, message) => Console.WriteLine(message?.Event));
await room.Subscribe();
await broadcast.Send("cursor", new BaseBroadcast { Payload = new() { { "x", 10 } } });
room.Unsubscribe();With AutoConnectRealtime = true the socket connects in InitializeAsync() and uses the signed-in user's token, so Row Level Security applies to the changes you receive. Presence: room.Register<MyPresence>(presenceKey), then Track(...).
#Edge Functions
var json = await stitchdb.Functions.Invoke("hello-world", stitchdb.Auth.CurrentSession?.AccessToken,
new StitchDB.Functions.Client.InvokeFunctionOptions { Body = new() { { "name", "Ada" } } });
var hello = await stitchdb.Functions.Invoke<Hello>("hello-world", stitchdb.Auth.CurrentSession?.AccessToken);Pass the user's access token so the function knows who is calling (or null to call with the anon key).
#Errors
try
{
await stitchdb.From<Todo>().Insert(new Todo { Task = null! });
}
catch (StitchDB.Postgrest.Exceptions.PostgrestException e)
{
Console.WriteLine($"{e.StatusCode}: {e.Message}");
}
catch (StitchDB.Gotrue.Exceptions.GotrueException e)
{
Console.WriteLine(e.Reason); // e.g. UserBadPassword, UserEmailNotConfirmed
}Each service throws its own exception: PostgrestException, GotrueException (with a Reason), StitchDBStorageException, FunctionsException and RealtimeException. See Errors for the status codes.
#Tracing
The clients emit OpenTelemetry-compatible traces and metrics through System.Diagnostics; register them with .AddSource(StitchDBDiagnostics.SourceNames.ToArray()) and .AddMeter(...). No token or payload is recorded.
#App services
Since 1.1.0 the same client reaches every StitchDB app service (namespace StitchDB.Apps). Turn each service on in Studio first. In these examples admin is a client created with the service_role key, on a server only.
#Push notifications
using StitchDB.Apps;
// After sign-in and on every new token
await stitchdb.Push.Register(PushDevice.Android(fcmToken, new[] { "news" }));
await stitchdb.Push.Subscribe("sale");
// The in-app inbox
var inbox = await stitchdb.Push.Inbox(unread: true);
await stitchdb.Push.MarkAllRead();
using var live = await stitchdb.Push.OnNotification(n => ShowBanner(n.Title));
// Server side, with the service key
long id = await admin.Push.Send(new PushMessage { UserId = userId, Title = "Your order shipped" });
var status = await admin.Push.Status(id);Methods: Register, Unregister, Subscribe, Unsubscribe, Inbox, MarkRead, MarkAllRead, DeleteNotifications, OnNotification (over Realtime), Config, Send, Status. Devices: PushDevice.Android (FCM), PushDevice.Ios (APNs), PushDevice.Web. See Push notifications.
#In-app purchases
await stitchdb.Purchases.Sync(PurchaseInput.Google(purchaseToken, productId: "pro_monthly"));
await stitchdb.Purchases.Sync(PurchaseInput.Apple(signedTransaction: jws));
if (await stitchdb.Purchases.HasEntitlement("pro")) UnlockPro();Methods: Sync, Me, HasEntitlement, Config, and with the service key Refresh(userId). See In-app purchases.
#Feature flags
await stitchdb.Flags.Load(new Dictionary<string, object?> { ["app_version"] = "2.4.1" });
if (stitchdb.Flags.IsOn("new-checkout")) ShowNewCheckout();
var limit = stitchdb.Flags.Get("upload-limit-mb", 10);
stitchdb.Flags.Changed += (_, keys) => Refresh(keys);Methods: Load, Refresh, SetAttributes, Get<T>, IsOn, Detail, All, the Changed event, Stop, and with the service key Evaluate. See Feature flags.
#Analytics and crash reporting
stitchdb.Analytics.Start(release: "2.4.1");
stitchdb.Analytics.Track("purchase", new { plan = "pro" });
await stitchdb.Analytics.Flush();
stitchdb.Crashes.Start(release: "2.4.1", environment: "production"); // unhandled exceptions are reported
try { Risky(); }
catch (Exception e) { await stitchdb.Crashes.CaptureException(e); }Crash reporting catches unhandled and unobserved task exceptions, with breadcrumbs, issues, releases and source maps. See Analytics and Crash reporting.
#Deep links
var link = await stitchdb.Links.Resolve(openedUrl); // path and data, or null
var first = await stitchdb.Links.Deferred("android"); // the link that led to the installMethods: Resolve, Deferred, Create, List, Get (stats), Update, Delete. See Deep links.
#Search
var hits = await stitchdb.Search<Post>("posts", "running shoes", limit: 10, mode: "hybrid");
foreach (var post in hits.Data) Console.WriteLine(post.Title); // best first; hits.ScoresWithout a type, Search(table, q) returns JsonElement rows. Modes: hybrid, keyword, semantic. See Search.
#Offline sync
var offline = stitchdb.Offline(new[] { "todos" }, storePath: "todos.json");
await offline.Sync();
var todos = offline.Table("todos");
offline.Conflict += (_, conflict) => Console.WriteLine(conflict);A local copy with queued writes, and StatusChanged and Conflict events. See Offline sync.
#Organizations
var org = await stitchdb.Orgs.Create("Acme");
await stitchdb.Orgs.Invite(org.Id, "[email protected]", role: "admin");
await stitchdb.Orgs.SetActive(org.Id); // the session's org_id / org_role claimsMethods: Create, Update, Delete, List, Members, Invite, Invites, RevokeInvite, AcceptInvite, SetRole, RemoveMember, Leave, TransferOwnership, SetActive. See Organizations.
#Realtime message history
await stitchdb.History.Publish("room-1", "message", new { text = "hi" }); // stored, then broadcast
var page = await stitchdb.History.List("room-1", since: DateTimeOffset.UtcNow.AddHours(-1));
var all = await stitchdb.History.ListAll("room-1");Topics are private by default; pass isPrivate: false for a public channel. See Message history.
#Background jobs and job monitoring
var run = await admin.Jobs.Start("send-invoice", new { order = 42 }, idempotencyKey: "order-42");
var now = await admin.Jobs.Get(run.Id);
var logs = await admin.Jobs.Logs(run.Id);
// At the end of a scheduled task
await stitchdb.Heartbeat.Ping(heartbeatUrl); // or state: "fail", or PingExitCode(url, code)Jobs: Start, Get, List, Logs, Cancel, Retry. Heartbeat: Ping, PingExitCode, Monitors, Monitor. See Background jobs and Job monitoring.
#Usage metering
await admin.Metering.Record("api_calls", 1, customer: "acme", idempotencyKey: requestId);
var usage = await admin.Metering.Usage(customer: "acme", group: "month");
string csv = await admin.Metering.ExportCsv(meter: "api_calls");Methods: Record (one event or many), Usage, Customers, Export, ExportCsv, SetStripeCustomer, SetStripeCustomers, StripeCustomers, Alerts. See Usage metering.
#Collaborative documents
var docs = await stitchdb.Collab.List();
var updates = await stitchdb.Collab.Get("plan-2026"); // stored Yjs updates (updates.Bytes())
await stitchdb.Collab.PostUpdate("plan-2026", update);
await stitchdb.Collab.CreateSnapshot("plan-2026", "Before review");Over HTTP: List, Get, PostUpdate, Members, AddMember, RemoveMember, Snapshots, CreateSnapshot, GetSnapshot. See Collaborative documents.
#GraphQL
var r = await stitchdb.GraphQL("query($n: Int) { todosCollection(first: $n) { edges { node { id } } } }", new { n = 10 });
if (r.Errors is { Count: > 0 }) Console.WriteLine(r.Errors[0]);Returns GraphQLResponse with Data and Errors. See GraphQL.
#Status page
var status = await stitchdb.StatusPage.Status();
await stitchdb.StatusPage.Subscribe("[email protected]");See Status page.
#Serverless SQL
var sql = stitchdb.Sql(Environment.GetEnvironmentVariable("STITCHDB_SQL_URL")!); // or new SqlClient(url)
var orders = await sql.Query<Order>("select * from orders where id = $1", 42);
await sql.Transaction(new[]
{
new SqlStatement("update stock set n = n - 1 where sku = $1", sku),
new SqlStatement("insert into orders (sku) values ($1)", sku),
});Methods: Query, Query<T>, Execute (the full result), Transaction. See Serverless driver.
#Webhooks and agent keys
bool genuine = Webhooks.VerifyHeaders(body, request.Headers.Select(h => new KeyValuePair<string, string>(h.Key, h.Value.ToString())), secret);
var agent = new AgentClient(Environment.GetEnvironmentVariable("STITCHDB_AGENT_KEY")!); // sdb_agt_ keys
var result = await agent.Sql("select count(*) from orders", session: "task-1");Webhooks.Verify and VerifyHeaders check the StitchDB-Signature header with replay protection; Sign makes one for tests. See Webhooks and Agent keys.
#More auth and storage
await stitchdb.Auth.SignInWithWeb3("ethereum", message, signature);
var options = await stitchdb.Auth.StartPasskeyAuthentication();
await stitchdb.Auth.VerifyPasskeyAuthentication(challengeId, credential);
var codes = await stitchdb.Auth.CreateRecoveryCodes();
bool exists = await stitchdb.Storage.From("avatars").Exists("user-1/avatar.png");Auth also has StartPasskeyRegistration, VerifyPasskeyRegistration, ListPasskeys, UpdatePasskey, DeletePasskey, VerifyRecoveryCode, OAuth server consent (GetOAuthAuthorizationDetails, ApproveOAuthAuthorization, DenyOAuthAuthorization, ListOAuthGrants, RevokeOAuthGrant), and phone MFA factors. Storage adds CreateUploadSignedUrl(path, upsert) and bucket lifecycle rules (GetBucketLifecycle, UpdateBucketLifecycle, DeleteBucketLifecycle). See Web3 and Passkeys.