Installation and host integration

Copy Markdown View Source

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:

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:

mix igniter.install blog_engine

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

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:

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:

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.

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

{: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.

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

{: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.