diff --git a/apps/app/mix.exs b/apps/app/mix.exs
index 6ce144b5..3c8b99b8 100644
--- a/apps/app/mix.exs
+++ b/apps/app/mix.exs
@@ -44,6 +44,7 @@ defmodule App.MixProject do
       {:core, in_umbrella: true},
       {:ecto_sql, "~> 3.4"},
       {:excoveralls, "~> 0.10", only: [:dev, :test]},
+      {:oban, "~> 2.1"},
       {:phoenix, "~> 1.5.8"},
       {:phoenix_ecto, "~> 4.0"},
       {:phoenix_html, "~> 2.11"},
diff --git a/apps/core/guides/features/admin.md b/apps/core/guides/features/admin.md
index aad94154..38a7ec7d 100644
--- a/apps/core/guides/features/admin.md
+++ b/apps/core/guides/features/admin.md
@@ -1 +1,11 @@
 # Admin
+
+The admin interface is generated through [Kaffy](https://aesmail.github.io/kaffy/).
+You can find extensive documentation on the Kaffy site regarding how to use and
+customize your admin interface.
+
+Legendary specific notes:
+
+- The configuration for Kaffy in your Legendary app is located in config/admin.exs.
+- Many of the built-in schemas provide admin modules. You shouldn't generally
+need to change these, but you may want to do so if you are changing built-in schema modules.
diff --git a/apps/core/guides/features/auth.md b/apps/core/guides/features/auth.md
index e97b0679..7c530cbf 100644
--- a/apps/core/guides/features/auth.md
+++ b/apps/core/guides/features/auth.md
@@ -1 +1,71 @@
 # Authentication and Authorization
+
+Legendary provides a set of authentication and authorization features out of the
+box.
+
+# Authentication
+
+Legendary comes with authentication powered by [Pow](https://powauth.com/) out
+of the box. The default configuration:
+
+- supports sign in and registration with an email and password
+- allows password resets
+- requires users to confirm their email address before logging in
+- emails for email confirmation and password reset will be nicely styled using your app's
+email styles
+
+> Tip: in development mode, emails your app sends will be visible at http://localhost:4000/sent_emails.
+
+Your Pow configuration can be customized in config/config.exs.
+
+By default, users can be administrated in the admin interface.
+
+# Roles and Authorization
+
+Users have an array of roles. By default, a user has no roles, but they can have
+as many as you need. Roles in Legendary are arbitrary strings that you tag a user
+with to give them certain privileges.
+
+For example, here's a typical admin user created by the `mix legendary.create_admin` command:
+
+```elixir
+%Legendary.Auth.User{
+  email: "legendary@example.com",
+  homepage_url: nil,
+  id: 1,
+  inserted_at: ~N[2021-02-25 22:14:40],
+  # This user has one role-- admin!
+  roles: ["admin"],
+  updated_at: ~N[2021-02-25 22:14:40]
+}
+```
+
+`admin` happens to be a role that the framework cares about-- via the `mix legendary.create_admin` command and the `:require_admin` pipeline that protects
+the admin interface. However, you can use any string you want as a role and check
+for it in your code. For example, your app might give some users a `paid_customer`
+role and use it to protect certain features. You don't have to declare that in advance with the framework.
+
+In some cases, you may want "resourceful roles"-- a role that corresponds to a
+specific resource record in your app. We suggest the following convention for those
+role names: `:role_name/:resource_type/:id`. So that could be `owner/home/3` to
+indicate the user is the owner of the Home with the id of 3. An authorized guest
+to the same home might be `guest/home/3`.
+
+You can check whether a user has a role by calling Legendary.Auth.Roles.has_role?/2:
+
+```elixir
+Legendary.Auth.Roles.has_role?(user, "admin")
+```
+
+And you can always access the `user.roles` field directly.
+
+# Protected routes
+
+## Signed-In Only Routes
+
+You can require that a given route requires a user by piping through the `:require_auth` pipeline. See apps/app/lib/app_web/router.ex for examples.
+
+## Admin Only Routes
+
+You can lock down a route to the app to only admin users by using the `:require_admin` pipeline. For example, the /admin area of your app is protected
+that way. See apps/app/lib/app_web/router.ex for examples.
diff --git a/apps/core/guides/features/background-jobs.md b/apps/core/guides/features/background-jobs.md
index e6291265..44ae2d97 100644
--- a/apps/core/guides/features/background-jobs.md
+++ b/apps/core/guides/features/background-jobs.md
@@ -1 +1,13 @@
 # Background Jobs
+
+Background jobs and periodic jobs in Legendary are powered by [Oban](https://github.com/sorentwo/oban). See the Oban documentation for extensive information
+on using Oban in your application, including:
+
+- queue configuration
+- worker configuration
+- unique job constraints
+- periodic jobs
+
+The framework itself uses Oban for recurring tasks such as generating sitemaps.
+
+Your app's Oban configuration is available in config/config.exs.
diff --git a/apps/core/guides/features/content-management.md b/apps/core/guides/features/content-management.md
index 1a420e8e..68289805 100644
--- a/apps/core/guides/features/content-management.md
+++ b/apps/core/guides/features/content-management.md
@@ -1 +1,70 @@
 # Content Management
+
+Your app includes a basic content management system including a simple blog
+(including optional user comments), dynamic pages, and static pages.
+
+Pages and blog posts can be managed from the admin interface. Posts and pages
+support content in [Markdown](https://www.markdownguide.org/).
+
+# Blog Posts
+
+Your app has a blog at /blog. Your can create and manage posts from "Content > Pages and Posts"
+in the /admin area. You can write your post body in Markdown.
+
+By default, posts have a few fields:
+
+- Type: for a blog post, this will be "Blog Post"
+- Slug: this is the url path of your post. For example, a post with slug "hello-world"
+would be available at /hello-world.
+- Title: the human-readable title of your post.
+- Content: this is the body of your post as Markdown. The admin provides a nice
+editor in case you don't know Markdown syntax yet or don't want to bother.
+- Status: this is Publish if you want your post to be visible to everyone, or draft
+if you aren't ready to share it with the world.
+- Author: this will normally be you, but we do allow admins to ghost-write for other
+users.
+- Excerpt: A short summary of your blog post that may show up in search engine results.
+- Sticky: sticky posts will always show up first on your blog. They are generally used
+for important announcements and community rules.
+- Comment status: "open" will allow comments on your post. "closed" hides comments and
+does not allow new comments to be entered.
+- Ping status: whether the post supports (pingbacks)[https://en.wikipedia.org/wiki/Pingback].
+**Coming soon:** we don't currently show pingbacks anywhere or notify anyone when a
+pingback is received, but we may in the future.
+- Menu order: **Coming soon:** the relative order this blog post will show up in
+dynamic menus. Menu management is currently in development. The lower this number,
+the higher the post will appear in the menu.
+
+# Dynamic Pages
+
+Dynamic pages are very similar to blog posts. The only differences are that their
+type is "Page" instead of "Blog Post" and they do not appear in your blog feed.
+
+They are intended for simple pages that will be updated by your admins, but don't
+make sense as a blog post-- for example, terms of service or FAQ pages.
+
+The fields of dynamic pages are the same as blog posts.
+
+# Static Pages
+
+Legendary also supports static pages. Static pages are not editable from the admin.
+However, they provide an easy way for developers on a Legendary app to create
+and serve a content page without defining custom controllers and routes. This is
+a good fit for pages that are more complex than what can be done with Markdown
+in dynamic pages.
+
+Static pages are eex templates located in apps/content/lib/content_web/templates/posts/static_pages/.
+For example, the home page of your app is a static page called index.html.eex.
+The filename, less the .html.eex part, serves as the slug. In other words, a
+static page called pricing.html.eex would have the url path /pricing in your app.
+
+> Note: if a static page and a dynamic page have the same slug, the dynamic page
+> will "win." This allows you to provide a default version of the page as a fallback
+> in code, while allowing admins to create an updated version of the same page.
+
+# Comments
+
+As mentioned above, blog posts can optionally have comments enabled. On these posts,
+there will be a feed of comments as well as a comment form at the bottom of the page.
+
+Comments can be managed by admins in the admin interface under "Content > Comments."
diff --git a/apps/core/guides/features/devops-templates.md b/apps/core/guides/features/devops-templates.md
index 3e40f583..dea54d0a 100644
--- a/apps/core/guides/features/devops-templates.md
+++ b/apps/core/guides/features/devops-templates.md
@@ -1 +1,55 @@
 # DevOps Templates
+
+Legendary includes a full set of DevOps templates designed to make it easy to
+test, build, and deploy your app.
+
+# Overview
+
+The setup we provide is an opinionated setup based on years of experience building
+Phoenix applications and deploying them at scale. It's meant to be efficient and
+easy for small teams while scaling to big teams. It's meant to be lean enough
+for low-traffic apps while scaling quite well to apps receiving thousands of
+requests a second.
+
+Here's the process overview:
+
+1. You make commits using [conventional commit messages](https://www.conventionalcommits.org/).
+2. The CI runs tests and builds a Docker image unique to your latest commit.
+3. Should your tests pass, that Docker image is labeled with a [semantic version](https://semver.org/)
+driven by your commit messages. We also tag that commit with that version so that
+you can refer to it. You never need to manually tag Docker image or a commit, so
+long as you follow the commit message convention.
+4. You can deploy that Docker image to Kubernetes, or any other Docker-friendly hosting environment.
+    - CI generates a Kubernetes manifest pointing at that new docker image. You can
+      apply that manifest to your cluster manually, or use a tool like flux2 to
+      automate that.
+    - If you don't use Kubernetes, you can tell your Docker-ized host to pull the new image in the method provided by that host.
+
+# CI Configuration
+
+Legendary comes with GitLab CI settings which should work for you with minimal
+setup. This config is located in .gitlab-ci.yml.
+
+The CI script will automatically tag successful builds. To do this, you will
+need to configure a [CI variable](https://docs.gitlab.com/ee/ci/variables/) named
+`GITLAB_TOKEN`. This token should be a
+[personal access token](https://gitlab.com/-/profile/personal_access_tokens) with
+`read_repository, write_repository` permissions.
+
+This CI configuration provides a few nice features:
+
+- Parallel build steps. The tests run while the Docker image builds, so you don't
+have to wait for one then the other.
+- Fast Docker build configuration. We use Docker BuildKit and a heavily tuned Dockerfile to reduce builld times from 15+ minutes to ~3 minutes.
+- Fast Elixir compile times. Out of the box, Elixir compilation can be quite
+slow in CI. We employ a few tricks to reduce the compilation time by over 75%
+over default CI configuration.
+- Automated semantic versioning. So long as you use conventional commit messages,
+we will automatically bump the version number appropriately.
+
+# Kubernetes Manifests
+
+We also automatically generate a Kubernetes manifest for your app on each successful build. The generate manifest is commited back to your repo at infrastructure/. You can use a tool like flux2 to automatically update the configuration in your Kubernetes cluster from there. Or you could manually apply
+it whenever you choose.
+
+The template used to generate the manifest is located in infrastructure_templates. Feel free to customize it if your application needs different Kubernetes config.
diff --git a/apps/core/guides/features/email.md b/apps/core/guides/features/email.md
new file mode 100644
index 00000000..27d2a8bc
--- /dev/null
+++ b/apps/core/guides/features/email.md
@@ -0,0 +1,80 @@
+# Email
+
+# Fluid Email Templates
+
+We provide an email template based on
+[Cerberus's Fluid Template](https://tedgoas.github.io/Cerberus/#fluid). This
+is a template well-suited for transactional email that has been well-tested on
+a wide variety of email clients. It should let you send nice looking email from
+your app without having to think about it a lot.
+
+# Branding / Theming
+
+Of course, you might want to customize the style of your emails to match your app's
+unique look or brand. The trick is that for emails to really work across a broad
+set of common clients, they need to _inline their CSS_. We take care of this for
+you.
+
+You can customize the variables (colors, sizes, etc) in config/email_styles.exs
+and we'll apply them to your emails.
+
+# Mailer
+
+Of course, you may want to send your own emails. We provide two modules to help:
+
+- Legendary.CoreEmail: responsible for generating emails to your specifications
+- Legendary.CoreMailer: responsible for sending emails per your configuration
+
+Both are powered by [Bamboo](https://github.com/thoughtbot/bamboo) so you
+can follow the Bamboo documentation to learn more about customizing and using
+email in your app.
+
+Here's an example:
+
+```elixir
+defmodule App.HelloEmail do
+  import Bamboo.Email
+  use Bamboo.Phoenix, view: AppWeb.EmailView
+
+  def send_hello_email(to) do
+    to_address
+    |> hello_email()
+    |> Legendary.CoreMailer.deliver_later()
+  end
+
+  def hello_email(to_address) do
+    Legendary.CoreEmail.base_email()
+    |> to(to_address)
+    |> render(:hello, to_address: to_address)
+  end
+end
+```
+
+> Tip: in development mode, any email you send can be viewed at localhost:4000/sent_emails.
+
+# Email Helpers
+
+Fluid email templates don't do any good if the content of your HTML emails isn't also as fluid and well-tested. We provide email tag helpers so that you don't
+have to hand-craft email-friendly HTML. See `Legendary.CoreWeb.EmailHelpers`.
+
+For example, your hello.html.eex might look something like this:
+
+```eex
+<%= preview do %>
+  Have you heard of our awesome app?
+<% end %>
+
+<%= h1 do %>
+  Hello, <%= to_address %>
+<% end %>
+<%= p do %>
+  We hope you'll join us.
+<% end %>
+
+<%= styled_button href: "http://example.com/" do %>
+  Join us!
+<% end %>
+```
+
+We'll handle generating all of the nested tags and inline CSS needed to make the
+email look good.
diff --git a/apps/core/guides/features/fluid-email-templates.md b/apps/core/guides/features/fluid-email-templates.md
deleted file mode 100644
index 59585663..00000000
--- a/apps/core/guides/features/fluid-email-templates.md
+++ /dev/null
@@ -1 +0,0 @@
-# Fluid Email Templates
diff --git a/apps/core/guides/features/i18n.md b/apps/core/guides/features/i18n.md
index 05b5b619..dbdb29b1 100644
--- a/apps/core/guides/features/i18n.md
+++ b/apps/core/guides/features/i18n.md
@@ -1 +1,24 @@
-# I18n
+# Strings File and I18n
+
+It's a good idea to extract any human-readable strings in your application out
+into a configuration file. The reason is two-fold:
+
+1. It makes it easier for developers to update "[copy](https://en.wikipedia.org/wiki/Copy_(written))" in the application and even
+allows non-developers on a team to make copy changes.
+2. When your application supports multiple languages, it is easy for translators
+to provide translations for all of your copy at once.
+
+In Legendary, we provide a set of tools for doing this.
+
+- (English) strings are stored in config/i18n/en.yml.
+- You can call `Legendary.I18n.t!/2` to get a string by its key. For example: `Legendary.I18n.t! "en", "site.title"` retrieves the english version of the
+string labeled "title" under the section "site" on en.yml.
+
+> Tip: if you use t! a lot (good job!), you can import it in your view module
+> to save some typing like `import Legendary.I18n, only: [t!: 2]` and then use it like `<%= t! "en", "site.title" %>` in your templates.
+
+Note that the first argument is a two-letter language code. In order to support
+other languages, you can provide more yml files in config/i18n (example, config/i18n/fr.yml for French) and call t!/2 with that language code instead.
+
+**On the roadmap:** in the future, we intend to provide a mechanism for detecting
+and managing each visitor's language and providing those strings if available.
diff --git a/apps/core/guides/features/tasks-and-scripts.md b/apps/core/guides/features/tasks-and-scripts.md
index adde2d85..ba1c4875 100644
--- a/apps/core/guides/features/tasks-and-scripts.md
+++ b/apps/core/guides/features/tasks-and-scripts.md
@@ -1 +1,22 @@
 # Tasks and Scripts
+
+Legendary follows the [scripts to rule them all pattern](https://github.com/github/scripts-to-rule-them-all). This allows any developers familiar with the pattern,
+either from other Legendary projects, or from other projects that use the pattern,
+to immediately pick up a project and get it running.
+
+Here's a summary of the scripts you'll mostly use:
+
+1. **bootstrap** installs all the dependencies needed to run the project.
+2. **update** is used to update dependencies.
+3. **server** runs the server.
+4. **console** runs the interactive console.
+5. **test** runs the test suite.
+
+When you run server, console, or test, the script will make sure all the right
+dependencies are in place (by running bootstrap or update). This means you can
+go straight to running script/server and it should just work.
+
+We encourage you to customize these scripts to the needs of your project as it grows. A developer should only _ever_ have to run script/server to run the server,
+and should not need to remember anything beyond that. script/bootstrap should always install everything you need to set up the project from scratch. If you
+find yourself updating setup steps in your project's README.md, consider how you
+might automate away that setup in your scripts.
diff --git a/apps/core/guides/overview.md b/apps/core/guides/overview.md
index 0ff8f53b..30f64e32 100644
--- a/apps/core/guides/overview.md
+++ b/apps/core/guides/overview.md
@@ -66,3 +66,11 @@ need to configure a [CI variable](https://docs.gitlab.com/ee/ci/variables/) name
 `GITLAB_TOKEN`. This token should be a
 [personal access token](https://gitlab.com/-/profile/personal_access_tokens) with
 `read_repository, write_repository` permissions.
+
+## DevOps
+
+The preconfigured CI pipeline generates semantically versioned docker images that
+you can deploy in your choice of dockerized hosting. We also provide a manifest
+for Kubernetes that is automatically updated with each version (see infrastructure/
+for the generated result and infrastructure_templates/ for the templates used to
+generate the manifest).
diff --git a/apps/core/mix.exs b/apps/core/mix.exs
index 4fb14ee3..82878f24 100644
--- a/apps/core/mix.exs
+++ b/apps/core/mix.exs
@@ -57,7 +57,7 @@ defmodule Legendary.Core.MixProject do
       "guides/features/background-jobs.md",
       "guides/features/content-management.md",
       "guides/features/devops-templates.md",
-      "guides/features/fluid-email-templates.md",
+      "guides/features/email.md",
       "guides/features/i18n.md",
       "guides/features/tasks-and-scripts.md",
     ]
diff --git a/config/config.exs b/config/config.exs
index 669b11f0..b39ed293 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -66,6 +66,13 @@ config :content,
     {"0 * * * *", Legendary.Content.Sitemaps},
   ]
 
+config :app,
+  Oban,
+  repo: App.Repo,
+  queues: [default: 10],
+  crontab: [
+  ]
+
 import_config "email_styles.exs"
 import_config "admin.exs"