# Installation and host integration

BlogEngine requires Ecto, Ecto SQL, Postgrex, and an already available PostgreSQL Repo. It does not
provision or start a database. HTTP, JSON, OpenAPI, frontend integrations, and local database provisioning
are responsibilities of the host application.

## Dependency, Repo, and prefix

Add `{:blog_engine, "~> 0.1.0"}` to the host's dependencies. When the host configures exactly one Repo
in `:ecto_repos`, the installer selects it; hosts with zero, multiple, or dynamic Repo declarations
must pass `--repo` explicitly. Configure the same Repo and prefix for runtime calls:

```elixir
config :my_app, ecto_repos: [MyApp.Repo]
config :blog_engine, repo: MyApp.Repo, prefix: "blog_engine"
```

The prefix must be a safe PostgreSQL identifier and is embedded in every generated migration.
Changing configuration later does not relocate existing data. Treat prefix relocation as a planned
database migration with a tested backup and restore procedure.

### The application-environment fallback supports one host per BEAM VM

`config :blog_engine, ...` is a single global set of defaults, so it can describe exactly one host:
one `:repo`, one `:prefix`, and one `:authorizer` / `:route_registry` / `:notifier` / `:clock`. Any
deployment that runs more than one BlogEngine host inside the same BEAM VM — an umbrella with two
apps using BlogEngine, or a single node serving several products from different schemas — must pass
`repo:`, `prefix:`, and the adapter modules explicitly on every `BlogEngine.Context.new/1` call.
There is no per-host application-environment namespace; relying on the global fallback in that
situation silently gives every host the last-configured Repo and prefix.

Application configuration feeds the **Elixir API only**. No mix task resolves its Repo or prefix
from `config :blog_engine`: the tasks take `--repo` and `--prefix` flags, `--prefix` defaults to the
literal `"blog_engine"`, and `--repo` falls back to the host project's own `:ecto_repos`. (The
installer reads existing `config :blog_engine` solely to stay idempotent when it writes that
configuration.) Hosts with zero, multiple, or dynamic Repo declarations must pass `--repo` on every
task invocation, and a host whose runtime prefix differs from `"blog_engine"` must pass `--prefix`
to every task as well — configuring it only under `config :blog_engine` will not reach the
generators.

## Initial setup

Igniter hosts can add the dependency and invoke the installer in one command:

```console
mix igniter.install blog_engine
```

When BlogEngine is already a dependency, configure a specific Repo and prefix with:

```console
mix blog_engine.install --repo MyApp.Repo --prefix blog_engine
```

The installer supports Igniter's `--dry-run`, preserves host-owned configuration, and is
idempotent when the configuration and pinned same-prefix wrapper are already equivalent. It
preflights configuration and existing wrappers before writing. Igniter writes configuration
before it runs the queued ordinary Ecto task, however, so an `ecto.gen.migration` failure can leave
a **partial installation** containing configuration but no migration. Correct the generator
failure and rerun the same install command; do not assume queued task failures roll back files.
If BlogEngine was first compiled without Igniter and Igniter is added later, compile Igniter, then
rebuild the guarded installer with `mix deps.compile`,
`mix deps.clean blog_engine --build`, and `mix deps.compile blog_engine`.

Generate through the host's Ecto migration generator, review the new file, then migrate normally:

```console
mix blog_engine.setup --repo MyApp.Repo --prefix blog_engine
mix ecto.migrate
mix blog_engine.check_schema --repo MyApp.Repo --prefix blog_engine
```

The generated wrapper pins the package's current numeric migration version and rolls back to zero.
Neither BlogEngine generator executes migrations. Back up production data before installation,
rollback, upgrade, or prefix relocation.

### Rolling back leaves the PostgreSQL schema in place

`v01` creates the configured schema with `CREATE SCHEMA IF NOT EXISTS`, but its down migration
deliberately stops after dropping BlogEngine's own tables, view, and constraints — it never issues
`DROP SCHEMA`. The schema is a host-owned namespace that may be shared with host tables or another
library, so removing it is not BlogEngine's decision to make. After a full rollback, expect an
empty-but-present schema; drop it yourself only when you know nothing else lives in it.

## Upgrades and diagnostics

When a later package version adds an adjacent schema version, generate and review its host wrapper:

```console
mix blog_engine.gen.migration --repo MyApp.Repo --prefix blog_engine --from 1 --to 2
mix ecto.migrate
```

Only available, adjacent transitions are accepted. `mix blog_engine.check_schema` is read-only: it
starts only the selected Repo and its Ecto/Postgrex dependencies, reads the tracking comment and
PostgreSQL catalogs, and reports missing or changed tables, columns, indexes, and constraints. It
does not create, repair, migrate, or drop anything.

## Explicit Elixir context

Every operation receives a `BlogEngine.Context` containing tenant identity and host adapters. The
actor and authorization scope are command intent, not trusted authority; host authorization must
re-query identities and roles.

```elixir
context =
  BlogEngine.Context.new(
    repo: MyApp.Repo,
    prefix: "blog_engine",
    tenant_key: tenant.id,
    actor: %{type: "user", key: user.id},
    authorization_scope: %{publisher_type: "publication", publisher_key: publisher.id},
    authorizer: MyApp.BlogAuthorizer,
    route_registry: MyApp.BlogRouteRegistry,
    notifier: MyApp.BlogNotifier,
    clock: MyApp.Clock
  )
```

## Authoring, review, routes, and syndication

```elixir
{:ok, blog} =
  BlogEngine.Blogs.create(context, %{
    publisher_type: "publication",
    publisher_key: publisher.id,
    name: "News",
    publication_policy: :review_required,
    index_route_template: "/news",
    post_route_template: "/news/:post_slug"
  })

{:ok, author} = BlogEngine.Authors.create(context, %{name: "Editor", slug: "editor"})

{:ok, post} =
  BlogEngine.Posts.create_draft(context, blog.id, %{
    author_id: author.id,
    slug: "opening-day",
    title: "Opening day",
    content_markdown: "# Welcome"
  })
```

Review-required hosts submit, then approve the exact immutable revision. **Approval is the
publication step on a `:review_required` blog.** `approve_revision/3` marks the revision approved,
points the post at it as its published revision, clears the working revision, promotes the draft
route to canonical current state, and emits `revision.approved`, `post.published`, and
`route.changed` in that order. Do not call `BlogEngine.Posts.publish/2` afterwards: that command
requires a `:self_publish` blog and returns `{:error, %BlogEngine.Error{code: :review_required}}`
here. Publishing preserves an older canonical route as a former redirect when the slug changes.

```elixir
{:ok, submitted} = BlogEngine.Posts.submit(context, post.id)

# Approving publishes: `published` already has publication_state: :published.
{:ok, published} =
  BlogEngine.Posts.approve_revision(context, post.id, submitted.working_revision_id)

{:ok, %{post: ^published}} = BlogEngine.Routing.resolve(context, "/news/opening-day")
```

`BlogEngine.Posts.publish/2` is the self-publish path: on a `:self_publish` blog it approves the
working revision and publishes it in the same transaction, with no separate review step.

An entity post can request placement on another blog; a host-authorized curator approves it:

```elixir
{:ok, placement} = BlogEngine.Syndication.request(context, published.id, target_blog.id)
{:ok, placement} = BlogEngine.Syndication.approve(curator_context, placement.id)
```

## Host extension behaviours

BlogEngine calls four host-selected behaviours:

- `BlogEngine.Authorizer` decides a named command after validating actor shape. It must re-query
  host roles, publisher ownership, and eligibility rather than trusting caller-supplied maps.
- `BlogEngine.RouteRegistry` validates claims against routes outside BlogEngine within the same
  database transaction. The internal route uniqueness constraint remains the final race guard.
- `BlogEngine.Notifier` receives committed events as `notify(context, event)`, matching the other
  behaviours. Delivery is post-commit; durable external retries belong to the host.
- `BlogEngine.Clock` supplies UTC time for deterministic scheduling and tests.

The default authorizer denies commands. The default internal route registry protects BlogEngine's
own routes, the default notifier is a no-op, and the default clock uses system UTC time.

Uploads and asset storage are also host concerns: store a URL through revision fields only after
the host has authenticated, authorized, validated, and persisted the asset.
