{"id":34707926,"url":"https://github.com/mobiletoly/go-oversync","last_synced_at":"2026-04-04T22:06:46.348Z","repository":{"id":315125325,"uuid":"1058172125","full_name":"mobiletoly/go-oversync","owner":"mobiletoly","description":"PostgreSQL adapter for multi-device sync","archived":false,"fork":false,"pushed_at":"2026-04-03T03:53:49.000Z","size":1913,"stargazers_count":3,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-03T11:26:01.537Z","etag":null,"topics":["multi-device","postgresql","sqlite","sync"],"latest_commit_sha":null,"homepage":"https://mobiletoly.github.io/go-oversync/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mobiletoly.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE.txt","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-16T18:08:03.000Z","updated_at":"2026-04-03T03:53:52.000Z","dependencies_parsed_at":"2025-09-16T22:12:06.016Z","dependency_job_id":"32b67177-3a1b-4cd0-9a42-2ebef1c003b7","html_url":"https://github.com/mobiletoly/go-oversync","commit_stats":null,"previous_names":["mobiletoly/go-oversync"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/mobiletoly/go-oversync","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mobiletoly%2Fgo-oversync","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mobiletoly%2Fgo-oversync/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mobiletoly%2Fgo-oversync/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mobiletoly%2Fgo-oversync/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mobiletoly","download_url":"https://codeload.github.com/mobiletoly/go-oversync/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mobiletoly%2Fgo-oversync/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31416486,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-04T20:09:54.854Z","status":"ssl_error","status_checked_at":"2026-04-04T20:09:44.350Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["multi-device","postgresql","sqlite","sync"],"created_at":"2025-12-24T23:32:31.818Z","updated_at":"2026-04-04T22:06:46.341Z","avatar_url":"https://github.com/mobiletoly.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# go-oversync\n\ngo-oversync is a Go library suite for two-way sync between local SQLite clients and PostgreSQL\nservers.\n\nThe current contract is bundle-based:\n\n- PostgreSQL business tables are authoritative.\n- clients push one logical dirty set at a time through staged push sessions\n- clients pull complete committed bundles only\n- fresh installs and prune recovery rebuild from frozen snapshot sessions\n\n## Packages\n\n- `oversync/`: PostgreSQL adapter, schema validation, bundle capture, HTTP handlers\n- `oversqlite/`: SQLite client SDK with trigger-based dirty capture and sync loops\n- `examples/nethttp_server/`: reference `net/http` server\n- `examples/mobile_flow/`: end-to-end simulator for the current client/server contract\n- `examples/samplesync_server/`: sample server for the KMP sample app\n- `docs/`: Jekyll site content\n- `swagger/two_way_sync.yaml`: OpenAPI description of the HTTP surface\n\n## Current Supported Envelope\n\nServer-side registered tables are intentionally constrained:\n\n- one sync key column per registered table\n- visible sync key type must be `uuid` or `text`\n- registered PostgreSQL tables must include `_sync_scope_id TEXT NOT NULL`\n- registered PostgreSQL row identity must be scope-bound through `(_sync_scope_id, sync_key)`\n- registered tables must be FK-closed\n- registered-to-registered foreign keys must be scope-inclusive and `DEFERRABLE`\n- unsupported key shapes or FK shapes fail during bootstrap\n\nThe SQLite client is likewise fail-closed:\n\n- one configured remote schema per SQLite database\n- one `oversqlite.Client` process owner per SQLite database\n- managed local tables must be FK-closed\n\n## Quick Start\n\nInstall the module:\n\n```bash\ngo get github.com/mobiletoly/go-oversync\n```\n\nRun the fast checks:\n\n```bash\ngo test ./oversync ./oversqlite\n```\n\nStart the reference server:\n\n```bash\nDATABASE_URL=\"postgres://postgres:postgres@localhost:5432/clisync_example?sslmode=disable\" \\\nJWT_SECRET=\"dev-secret\" \\\ngo run ./examples/nethttp_server\n```\n\nRun an implemented simulator scenario:\n\n```bash\ncd examples/mobile_flow\ngo run . --scenario=fresh-install --cleanup=false\n```\n\n## Server Integration\n\n```go\ncfg := \u0026oversync.ServiceConfig{\n    MaxSupportedSchemaVersion: 1,\n    AppName:                   \"my-sync-app\",\n    RegisteredTables: []oversync.RegisteredTable{\n        {Schema: \"business\", Table: \"users\", SyncKeyColumns: []string{\"id\"}},\n        {Schema: \"business\", Table: \"posts\", SyncKeyColumns: []string{\"id\"}},\n    },\n}\n\nsvc, err := oversync.NewRuntimeService(pool, cfg, logger)\nif err != nil {\n    log.Fatal(err)\n}\nif err := svc.Bootstrap(ctx); err != nil {\n    log.Fatal(err)\n}\n\nhandlers := oversync.NewHTTPSyncHandlers(svc, logger)\n\nmux := http.NewServeMux()\nmux.Handle(\"POST /sync/connect\", auth(http.HandlerFunc(handlers.HandleConnect)))\nmux.Handle(\"POST /sync/push-sessions\", auth(http.HandlerFunc(handlers.HandleCreatePushSession)))\nmux.Handle(\"POST /sync/push-sessions/{push_id}/chunks\", auth(http.HandlerFunc(handlers.HandlePushSessionChunk)))\nmux.Handle(\"POST /sync/push-sessions/{push_id}/commit\", auth(http.HandlerFunc(handlers.HandleCommitPushSession)))\nmux.Handle(\"DELETE /sync/push-sessions/{push_id}\", auth(http.HandlerFunc(handlers.HandleDeletePushSession)))\nmux.Handle(\"GET /sync/committed-bundles/{bundle_seq}/rows\", auth(http.HandlerFunc(handlers.HandleGetCommittedBundleRows)))\nmux.Handle(\"GET /sync/pull\", auth(http.HandlerFunc(handlers.HandlePull)))\nmux.Handle(\"POST /sync/snapshot-sessions\", auth(http.HandlerFunc(handlers.HandleCreateSnapshotSession)))\nmux.Handle(\"GET /sync/snapshot-sessions/{snapshot_id}\", auth(http.HandlerFunc(handlers.HandleGetSnapshotChunk)))\nmux.Handle(\"DELETE /sync/snapshot-sessions/{snapshot_id}\", auth(http.HandlerFunc(handlers.HandleDeleteSnapshotSession)))\nmux.Handle(\"GET /sync/capabilities\", auth(http.HandlerFunc(handlers.HandleCapabilities)))\nmux.HandleFunc(\"GET /syncx/health\", handlers.HandleHealth)\nmux.HandleFunc(\"GET /syncx/status\", handlers.HandleStatus)\n```\n\nYour auth middleware must authenticate the request and inject `oversync.Actor` into request\ncontext before calling the sync handlers. `POST /sync/connect` requires `Actor.UserID`; push,\npull, and snapshot flows continue to rely on both `Actor.UserID` and `Actor.SourceID`. The runtime\nderives `_sync_scope_id` from `Actor.UserID`; clients never send or receive `_sync_scope_id` in\nvisible sync payloads.\n\n## SQLite Client\n\n```go\ncfg := oversqlite.DefaultConfig(\"business\", []oversqlite.SyncTable{\n    {TableName: \"users\", SyncKeyColumnName: \"id\"},\n    {TableName: \"posts\", SyncKeyColumnName: \"id\"},\n})\n\nclient, err := oversqlite.NewClient(db, \"http://localhost:8080\", tokenProvider, cfg)\nif err != nil {\n    log.Fatal(err)\n}\ndefer client.Close()\n\nif err := client.Open(ctx, \"device-abc\"); err != nil {\n    log.Fatal(err)\n}\n\nconnectResult, err := client.Attach(ctx, \"user-123\")\nif err != nil {\n    log.Fatal(err)\n}\nif connectResult.Status == oversqlite.AttachStatusRetryLater {\n    log.Printf(\"connect pending, retry after %s\", connectResult.RetryAfter)\n    return\n}\n\nif err := client.Sync(ctx); err != nil {\n    log.Fatal(err)\n}\n\nif err := client.Detach(ctx); err != nil {\n    log.Fatal(err)\n}\n```\n\n## Documentation\n\n- docs site: \u003chttps://mobiletoly.github.io/go-oversync/\u003e\n- getting started: `docs/getting-started.md`\n- server reference: `docs/documentation/server.md`\n- client reference: `docs/documentation/client.md`\n- HTTP API reference: `docs/documentation/api.md`\n\n## Examples\n\n- `examples/nethttp_server/`: reference server with JWT auth and test helpers\n- `examples/mobile_flow/`: simulator for implemented sync scenarios plus a small number of still-partial CLI entries\n- `examples/samplesync_server/`: sample server used by the Kotlin sample app\n\n## License\n\nApache 2.0. See `LICENSE` if present in your distribution or the source headers in this repository.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmobiletoly%2Fgo-oversync","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmobiletoly%2Fgo-oversync","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmobiletoly%2Fgo-oversync/lists"}