Rindle

Media, made durable.

Phoenix/Ecto-native media lifecycle library. Rindle owns the durable work that happens after upload: session tracking, verification, asset state, variants, background processing, signed delivery, and cleanup.

README.md is the narrow quickstart. guides/getting_started.md is the canonical deep adopter guide for the same first-run path.

Install

Add Rindle to your deps:

def deps do
  [
    {:rindle, "~> 0.1"}
  ]
end

If you use the S3 adapter, also choose an ExAws HTTP client. :hackney is the most-tested path in this repo:

def deps do
  [
    {:rindle, "~> 0.1"},
    {:hackney, "~> 1.20"}
  ]
end

Run mix deps.get.

Runtime Ownership

Rindle persists through your adopter-owned Repo. Configure that explicitly:

config :rindle, :repo, MyApp.Repo

Rindle also requires the default Oban path for background work. Adopters own the Oban supervision tree, queue config, and default Oban Repo:

config :my_app, Oban,
  repo: MyApp.Repo,
  queues: [
    rindle_promote: 5,
    rindle_process: 10,
    rindle_purge: 2,
    rindle_maintenance: 1
  ]

Migrations

Run your host app migrations and the packaged Rindle migrations explicitly. The consumer smoke lane proves this Application.app_dir/2 path from a generated Phoenix app:

rindle_path = Application.app_dir(:rindle, "priv/repo/migrations")
host_path = Path.join([File.cwd!(), "priv", "repo", "migrations"])

{:ok, _, _} =
  Ecto.Migrator.with_repo(MyApp.Repo, fn repo ->
    for path <- [host_path, rindle_path] do
      Ecto.Migrator.run(repo, path, :up, all: true)
    end
  end)

Rindle does not ship a public mix rindle.* install task for this in v1.1. The public path is the docs snippet above; the repo-private automation lives in the install smoke harness.

First Run: Presigned PUT

The first-run path is direct upload by presigned PUT. Multipart upload is supported, but it is an advanced capability and not the default onboarding story.

alias Rindle.Upload.Broker

{:ok, session} =
  Broker.initiate_session(MyApp.MediaProfile, filename: "photo.png")

{:ok, %{session: signed, presigned: presigned}} =
  Broker.sign_url(session.id)

# your client PUTs bytes to presigned.url

{:ok, %{session: completed, asset: asset}} =
  Broker.verify_completion(session.id)

{:ok, signed_url} =
  Rindle.Delivery.url(MyApp.MediaProfile, asset.storage_key)

That is the same public path proven by the built-artifact install smoke and the canonical adopter lifecycle test.

Next Reads

GSD Hygiene

For local GSD cleanup, run mix gsd.clean. It removes known transient outputs, prunes stale worktree metadata, and reports any remaining .planning/ dirt without deleting tracked planning artifacts.

Use the GSD workflows for the tracked planning lifecycle:

License

MIT