README.md

# 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`](guides/getting_started.md)
is the canonical deep adopter guide for the same first-run path.

## Install

Add Rindle to your deps:

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
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.

```elixir
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

- [`guides/getting_started.md`](guides/getting_started.md): canonical deep
  adopter guide for Repo ownership, Oban ownership, migrations, profile setup,
  and the same presigned PUT lifecycle
- [`guides/background_processing.md`](guides/background_processing.md): default
  Oban ownership and queue details
- [`guides/storage_capabilities.md`](guides/storage_capabilities.md): capability
  boundaries, including multipart as an advanced path

## 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:

- `$gsd-complete-milestone` when a milestone is actually done
- `$gsd-cleanup` to archive completed milestone phase directories
- `$gsd-pr-branch` to prepare a review branch without `.planning/` commits

## License

MIT