keinaufwand-sync

Reusable Rails persistence for the Keinaufwand API. The gem supports full/delta API sync, schema-backed Active Record models, SQLite and PostgreSQL JSON queries, local asset mirroring, generated JSON indexes, and signed incremental webhooks.

Part of Keinaufwand.

Install

gem "keinaufwand"
gem "keinaufwand-sync"
bin/rails generate keinaufwand:sync:install
bin/rails keinaufwand:indices:generate
bin/rails db:migrate

The install generator creates keinaufwand_records, keinaufwand_sync_states, keinaufwand_assets, and keinaufwand_webhook_receipts. It also mounts /media/:id and the webhook Engine at /webhooks/keinaufwand.

Configuration

Keinaufwand::Sync.configure do |config|
  config.client = -> { MyKeinaufwandClient.build }
  config.webhook_secret = -> { Rails.application.credentials.dig(:keinaufwand, :webhook_secret) }
  config.storage_adapter = :sqlite3
end

The consumer selects resources and JSON indexes in config/keinaufwand.yml. The versioned API schema remains bundled in keinaufwand.

bin/rails keinaufwand:sync
bin/rails keinaufwand:resync
bin/rails keinaufwand:indices:generate

Application models inherit from Keinaufwand::Record. Their id is the upstream ID; id and STI type form the composite primary key. Schema fields and associations use normal Active Record-style find, exists?, where, and order calls.

Webhooks

The mounted Engine accepts the exact JSON body signed by Keinaufwand in X-Keinaufwand-Signature. Processing is synchronous so a 200 response means the SQLite/PostgreSQL transaction committed. Event receipts make retries idempotent and preserve the latest source timestamp so an older delayed update cannot overwrite or resurrect newer data.

Regular keinaufwand:sync calls request each aggregate root with updated_since and apply _deleted records returned by widened soft-visibility scopes. Inline associations are stored inside the root JSON and exposed as schema-backed model objects without duplicate STI rows. Retried webhooks carry hard-delete tombstones; keinaufwand:resync repairs exceptional drift after a permanently failed delivery.

Polling compares source timestamps with both local records and webhook receipts after downloading media, then applies each page in a database transaction. Older polling snapshots cannot overwrite a newer webhook update or resurrect a record deleted by a newer webhook. Index generation skips unchanged indexes and uses distinct migration names for later configuration changes.