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.Authorizerdecides a named command after validating actor shape. It must re-query host roles, publisher ownership, and eligibility rather than trusting caller-supplied maps.BlogEngine.RouteRegistryvalidates claims against routes outside BlogEngine within the same database transaction. The internal route uniqueness constraint remains the final race guard.BlogEngine.Notifierreceives committed events asnotify(context, event), matching the other behaviours. Delivery is post-commit; durable external retries belong to the host.BlogEngine.Clocksupplies 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.