Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Docs: [Setting up the admin area](doc/how-to/SETTING_UP_ADMIN.md), a
step-by-step guide covering installation, the staff flag, tenant mode,
`before_admin_action`, ejecting, and adding your own dashboards. Every
step was checked in a generated app.
- Docs: the Deploying guide covers Rails 8's four production databases
(`CACHE_/QUEUE_/CABLE_DATABASE_URL`) and the Active Record encryption
keys the auth engine needs. The generated Kamal `deploy.yml` lists the
three extra database URLs.

### Fixed

- Admin engine: an app without every canonical engine (for example only
`core` and `auth`) got a 500 on every admin page, because the
navigation loaded dashboards whose models don't exist. Those
dashboards are now hidden and their URLs return 404.
- Design generator: the next-steps message now marks
`bin/rails tailwindcss:build` as required and names the error you get
without it.
- Admin README: the "add your own dashboard" recipe now includes
`self.model`, the policies, the leading-slash route, and deleting
Administrate's generated host controller. It no longer claims
`theme_css_path` restyles the admin, since that setting is not
applied yet.
- Docs: the onboarding path now works when followed literally. Both
tutorials and the README quick start were re-run in fresh apps from
the published gem. The 10-minute tutorial claimed SQLite works (the
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
- Requests for another account's records return 404.
- A `member` gets a 403.

The generated `engines/admin/README.md` has the full reference.
Step-by-step guide: [Setting up the admin area](doc/how-to/SETTING_UP_ADMIN.md). The generated `engines/admin/README.md` has the full reference.

Check warning on line 165 in README.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"README.md","range":{"start":{"line":165,"column":37},"end":{"line":165,"column":42}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}

Check warning on line 165 in README.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 'Setting' should be in lowercase. Raw Output: {"message":"'Setting' should be in lowercase.","location":{"path":"README.md","range":{"start":{"line":165,"column":22},"end":{"line":165,"column":29}}},"severity":"WARNING","code":{"value":"Google.Colons","url":"https://developers.google.com/style/colons"}}

</details>

Expand Down Expand Up @@ -198,6 +198,7 @@
- [Removing an engine](doc/how-to/REMOVING_AN_ENGINE.md)
- [Writing an adapter](doc/how-to/WRITING_AN_ADAPTER.md): swap in Mailgun, Twilio, Paddle, and others
- [Writing follow-up generators](doc/how-to/WRITING_FOLLOW_UP_GENERATORS.md)
- [Setting up the admin area](doc/how-to/SETTING_UP_ADMIN.md)

Check warning on line 201 in README.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"README.md","range":{"start":{"line":201,"column":19},"end":{"line":201,"column":24}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}
- [Insertion points](doc/reference/INSERTION_POINTS.md) and the [catalogue of markers](doc/reference/INSERTION_POINTS_CATALOGUE.md)
- [Deploying](doc/how-to/DEPLOYING.md)

Expand Down
1 change: 1 addition & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- [REMOVING_AN_ENGINE.md](how-to/REMOVING_AN_ENGINE.md) — remove an engine cleanly.
- [WRITING_AN_ADAPTER.md](how-to/WRITING_AN_ADAPTER.md) — swap in Mailgun, Twilio, Paddle, etc.
- [WRITING_FOLLOW_UP_GENERATORS.md](how-to/WRITING_FOLLOW_UP_GENERATORS.md) — extend an installed engine.
- [SETTING_UP_ADMIN.md](how-to/SETTING_UP_ADMIN.md) — add the admin area, let yourself in, tenant mode, your own dashboards.

Check warning on line 26 in doc/README.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"doc/README.md","range":{"start":{"line":26,"column":65},"end":{"line":26,"column":70}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}

Check failure on line 26 in doc/README.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Don't put a space before or after a dash. Raw Output: {"message":"Don't put a space before or after a dash.","location":{"path":"doc/README.md","range":{"start":{"line":26,"column":52},"end":{"line":26,"column":57}}},"severity":"ERROR","code":{"value":"Google.EmDash","url":"https://developers.google.com/style/dashes"}}
- [DEPLOYING.md](how-to/DEPLOYING.md) — ship a host to production.
- [UPGRADING_FROM_WAVE_8.md](how-to/UPGRADING_FROM_WAVE_8.md) — migrate a pre-Wave-9 host.

Expand Down
63 changes: 60 additions & 3 deletions doc/how-to/DEPLOYING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,63 @@
it doesn't rewrite the Dockerfile — but `bundle install` inside the
container picks up the new engine gemspec on the next image build.

## Databases in production (Rails 8)

A Rails 8 app has **four** production databases in `config/database.yml`:
`primary`, plus `cache`, `queue` and `cable` for Solid Cache, Solid Queue
and Solid Cable. `DATABASE_URL` only configures `primary`. Unless you
configure the other three, `db:prepare` creates the primary database and
then fails, because it tries a local socket for the rest:

```text
connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed
```

Give each of them a URL. Rails reads `<NAME>_DATABASE_URL` for a named
database. They can live on the same Postgres server as long as the
database names differ:

```bash
DATABASE_URL=postgres://app:secret@db.example.com/app_production
CACHE_DATABASE_URL=postgres://app:secret@db.example.com/app_production_cache
QUEUE_DATABASE_URL=postgres://app:secret@db.example.com/app_production_queue
CABLE_DATABASE_URL=postgres://app:secret@db.example.com/app_production_cable
```

Alternatively, edit the `production:` block of `config/database.yml` to
point all four at your server.

> Verified with a generated app running every engine against
> `postgres:18` in Docker. With all four URLs set, the container ran
> `db:prepare` and served requests.

## Encryption keys

The auth engine encrypts personal data (such as email addresses) with
Active Record encryption. Production needs the same three keys as
development:

```bash
bin/rails db:encryption:init # prints an active_record_encryption: block
bin/rails credentials:edit # paste it in (or credentials:edit --environment production)
```

Ship the key that decrypts those credentials as `RAILS_MASTER_KEY`, or
`config/credentials/production.key` if you use per-environment
credentials. Without the encryption keys, the app boots, but the first
sign-up or sign-in raises
`Missing Active Record encryption credential: active_record_encryption.deterministic_key`.

> [!WARNING]
> Keep these keys stable. Rotating or losing them makes existing
> encrypted rows unreadable. Follow the Rails
> [key rotation guide](https://guides.rubyonrails.org/active_record_encryption.html#rotating-keys)
> before changing them.

## Migrations

`bin/docker-entrypoint` runs `bundle exec rails db:prepare` on every
boot. That's idempotent — Rails skips already-applied migrations —
`bin/docker-entrypoint` runs `bundle exec rails db:prepare` when the
container starts the web server. Workers skip it. That's idempotent — Rails skips already-applied migrations —

Check failure on line 93 in doc/how-to/DEPLOYING.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Don't put a space before or after a dash. Raw Output: {"message":"Don't put a space before or after a dash.","location":{"path":"doc/how-to/DEPLOYING.md","range":{"start":{"line":93,"column":111},"end":{"line":93,"column":115}}},"severity":"ERROR","code":{"value":"Google.EmDash","url":"https://developers.google.com/style/dashes"}}

Check failure on line 93 in doc/how-to/DEPLOYING.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Don't put a space before or after a dash. Raw Output: {"message":"Don't put a space before or after a dash.","location":{"path":"doc/how-to/DEPLOYING.md","range":{"start":{"line":93,"column":68},"end":{"line":93,"column":73}}},"severity":"ERROR","code":{"value":"Google.EmDash","url":"https://developers.google.com/style/dashes"}}
but if you'd rather control migrations from a one-off task (Kamal's
`pre-deploy` hook, GitHub Actions step, etc.) comment that line out.

Expand All @@ -59,6 +112,9 @@
secret:
- RAILS_MASTER_KEY
- DATABASE_URL
- CACHE_DATABASE_URL
- QUEUE_DATABASE_URL
- CABLE_DATABASE_URL
- REDIS_URL
- STRIPE_SECRET_KEY
- STRIPE_WEBHOOK_SECRET
Expand All @@ -84,8 +140,9 @@

| Variable | Set when |
| --- | --- |
| `RAILS_MASTER_KEY` | Always. |
| `RAILS_MASTER_KEY` | Always. Decrypts the credentials that hold the Active Record encryption keys. |
| `DATABASE_URL` | Always (host-supplied). |
| `CACHE_DATABASE_URL`, `QUEUE_DATABASE_URL`, `CABLE_DATABASE_URL` | Rails 8 apps, unless `config/database.yml` points these databases elsewhere. See [Databases in production](#databases-in-production-rails-8). |
| `REDIS_URL` | If using Sidekiq / ActionCable Redis. |
| `STRIPE_SECRET_KEY` | If the Billing engine is installed. |
| `STRIPE_WEBHOOK_SECRET` | Same. |
Expand Down
140 changes: 140 additions & 0 deletions doc/how-to/SETTING_UP_ADMIN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Setting up the admin area

Check warning on line 1 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":1,"column":18},"end":{"line":1,"column":23}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}

This guide adds an admin area at `/admin` to your app, signs you in to

Check warning on line 3 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":3,"column":20},"end":{"line":3,"column":25}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}
it, and shows how to switch it to tenant mode or add your own
dashboards. For why it's built this way, see
[ARCHITECTURE_WAVE_11.md](../explanation/ARCHITECTURE_WAVE_11.md).

## What you need first

- The **`core`** and **`auth`** engines. Admin uses `Auth::Identity` for

Check warning on line 10 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'Admin'. Raw Output: {"message":"Use 'administrator' instead of 'Admin'.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":10,"column":42},"end":{"line":10,"column":47}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}
sign-in and `Core::AuditLog` for its audit trail.
- The other engines are optional. Admin ships dashboards for accounts,

Check warning on line 12 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'Admin'. Raw Output: {"message":"Use 'administrator' instead of 'Admin'.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":12,"column":35},"end":{"line":12,"column":40}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}
memberships, teams, invitations, notifications, plans, subscriptions,
invoices and lifetime passes. Each appears only when its engine is
installed. The rest are hidden and their URLs return 404.

## 1. Generate it

```bash
bin/seams admin
bundle install # admin adds administrate and pundit to your Gemfile
```

The generator mounts the engine at `/admin`. It adds no tables, so no
migration is needed.

## 2. Let yourself in

Only **staff** can open the admin area by default. Mark your identity as

Check warning on line 29 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":29,"column":29},"end":{"line":29,"column":34}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}
staff from a console:

```bash
bin/rails runner 'Auth::Identity.find_by!(email: "you@example.com").update!(staff: true)'
```

Start the app, sign in at `/auth/session/new`, then visit `/admin`:

| You are | You get |
| --- | --- |
| Signed out | Redirected to `/auth/session/new` |
| Signed in, not staff | 403 "Access denied" |
| Signed in, staff | The Identities dashboard, with a sidebar of every installed dashboard |

Every create, update and delete made through the admin area writes a

Check warning on line 44 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Use 'administrator' instead of 'admin'. Raw Output: {"message":"Use 'administrator' instead of 'admin'.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":44,"column":50},"end":{"line":44,"column":55}}},"severity":"WARNING","code":{"value":"Google.WordListCase","url":"https://developers.google.com/style/word-list"}}
`Core::AuditLog` row, with you as the actor.

## 3. Choose platform or tenant mode

**Platform mode** is the default. Staff see every account's data. Use it
for your own operators.

**Tenant mode** lets each customer's `admin` and `owner` members manage
their own account only. It needs three settings. Create
`config/initializers/seams_admin.rb`:

```ruby
Seams::Admin.configure do |c|
c.tenancy_scope = :tenant

# Let any signed-in identity past the gate. The tenant policies then
# decide what each role may do.
c.authenticator = ->(ctrl) { ctrl.current_identity.present? }

# Tell the admin area which membership the request belongs to. The
# admin controllers don't inherit your ApplicationController, so resolve
# it here. This example takes the identity's first active membership.
# Use however your app picks the current account.
c.current_membership_resolver = lambda do |ctrl|
identity = ctrl.current_identity
identity && Accounts::Membership.find_by(identity_id: identity.id, active: true)
end
end
```

In tenant mode:
- An `admin` or `owner` sees only their account's rows.
- Another account's records return 404.
- A `member` gets 403.
- Staff still see everything.

> [!NOTE]
> Tenant mode needs the `accounts` engine.

## 4. Add a check before every admin request

`before_admin_action` runs after the gate on every admin request. Use it
for two-factor checks, IP allow-lists, or extra logging:

```ruby
Seams::Admin.configure do |c|
c.before_admin_action = ->(ctrl) { ctrl.head(:forbidden) unless ctrl.request.remote_ip.start_with?("10.") }
end
```

## 5. Change a dashboard

Generated files are yours to edit. To keep your edits when the generator
runs again, eject the file first:

```bash
bin/seams resolve --eject admin/app/dashboards/admin/identity_dashboard.rb
```

Then edit `engines/admin/app/dashboards/admin/identity_dashboard.rb`.
Administrate's documentation covers fields and attribute lists:
<https://administrate-demo.herokuapp.com/>.

## 6. Add a dashboard for your own model

Adding a model such as `Project` takes a dashboard, a controller, two
policies, and a route. All of them live in the admin engine. The generated
`engines/admin/README.md` has the full recipe, under
"Customising a dashboard". In short:

Check failure on line 113 in doc/how-to/SETTING_UP_ADMIN.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Commas and periods go inside quotation marks. Raw Output: {"message":"Commas and periods go inside quotation marks.","location":{"path":"doc/how-to/SETTING_UP_ADMIN.md","range":{"start":{"line":113,"column":1},"end":{"line":113,"column":27}}},"severity":"ERROR","code":{"value":"Google.Quotes","url":"https://developers.google.com/style/quotation-marks"}}

1. **Dashboard.** Run `bin/rails generate administrate:dashboard Project`.
Move the dashboard to
`engines/admin/app/dashboards/admin/project_dashboard.rb`, wrap it in
`module Admin`, and add `def self.model = ::Project`.
2. **Controller.** Delete the controller Administrate generated in your
app's `app/controllers/admin/`, because it skips the admin gate.
Create `Admin::ProjectsController < ::Seams::Admin::ApplicationController`
in the engine instead.
3. **Policies.** Add `Admin::Platform::ProjectPolicy` and
`Admin::Tenant::ProjectPolicy`, each subclassing its namespace's
`ApplicationPolicy`. A dashboard without a policy is denied.
4. **Route.** Add `resources :projects, controller: "/admin/projects"`
inside the `scope as: :admin do` block in
`engines/admin/config/routes.rb`.

The sidebar picks up the new dashboard automatically.

## Troubleshooting

| Symptom | Cause | Fix |
| --- | --- | --- |
| `[seams admin] missing required dependency` at boot | The auth engine, `administrate`, or `pundit` is missing | `bin/seams auth`, then `bundle install` |
| 403 "Access denied" while signed in | The gate said no. By default it requires `staff?` | Set `staff: true` on your identity, or change `authenticator` |
| A dashboard returns 404 | Its engine isn't installed | Generate that engine, or ignore it; it stays hidden |
| Tenant admins get 403 everywhere | `authenticator` or `current_membership_resolver` isn't set | See [step 3](#3-choose-platform-or-tenant-mode) |
| Signing in fails with `Missing Active Record encryption credential` | No encryption keys | `bin/rails db:encryption:init`, then add the keys to credentials |
3 changes: 2 additions & 1 deletion doc/tutorials/GETTING_STARTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@
- [ENGINE_CATALOGUE.md](../reference/ENGINE_CATALOGUE.md) — The canonical engines in detail.
- [PERMISSIONS.md](../reference/PERMISSIONS.md) — Role → ability grant map and `authorize_permission!`.
- [DESIGN_SYSTEM.md](../design-system/DESIGN_SYSTEM.md) — The design engine: components, tokens, theming.
- [ARCHITECTURE_WAVE_11.md](../explanation/ARCHITECTURE_WAVE_11.md) — The admin engine.
- [SETTING_UP_ADMIN.md](../how-to/SETTING_UP_ADMIN.md) — Set up the admin area.

Check failure on line 156 in doc/tutorials/GETTING_STARTED.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Don't put a space before or after a dash. Raw Output: {"message":"Don't put a space before or after a dash.","location":{"path":"doc/tutorials/GETTING_STARTED.md","range":{"start":{"line":156,"column":55},"end":{"line":156,"column":60}}},"severity":"ERROR","code":{"value":"Google.EmDash","url":"https://developers.google.com/style/dashes"}}
- [ARCHITECTURE_WAVE_11.md](../explanation/ARCHITECTURE_WAVE_11.md) — Why the admin engine is built this way.

Check failure on line 157 in doc/tutorials/GETTING_STARTED.md

View workflow job for this annotation

GitHub Actions / Prose lint (advisory)

[vale] reported by reviewdog 🐶 Don't put a space before or after a dash. Raw Output: {"message":"Don't put a space before or after a dash.","location":{"path":"doc/tutorials/GETTING_STARTED.md","range":{"start":{"line":157,"column":68},"end":{"line":157,"column":73}}},"severity":"ERROR","code":{"value":"Google.EmDash","url":"https://developers.google.com/style/dashes"}}
- [CURRENT_ATTRIBUTES.md](../reference/CURRENT_ATTRIBUTES.md) — Per-request namespaces (Auth::Current, Accounts::Current, Teams::Current).
- [ARCHITECTURE.md](../explanation/ARCHITECTURE.md) — Why Seams is built this way.
- [UPGRADING_FROM_WAVE_8.md](../how-to/UPGRADING_FROM_WAVE_8.md) — If you adopted seams pre-Wave-9.
1 change: 1 addition & 0 deletions docs-site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export default defineConfig({
{ label: "Remove an engine", slug: "removing_an_engine" },
{ label: "Write an adapter", slug: "writing_an_adapter" },
{ label: "Write a follow-up generator", slug: "writing_follow_up_generators" },
{ label: "Set up the admin area", slug: "setting_up_admin" },
{ label: "Deploy", slug: "deploying" },
{ label: "Upgrade from Wave 8", slug: "upgrading_from_wave_8" },
],
Expand Down
67 changes: 50 additions & 17 deletions lib/generators/seams/admin/templates/README.md.tt
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,8 @@ Seams::Admin.configure do |c|
# at request time. See "Two-mode operation" above.
c.tenancy_scope = :platform

# 3. theme_css_path — Optional path to a host-supplied CSS file loaded
# on top of Administrate's stock styling. nil = use Administrate's
# defaults.
# 3. theme_css_path — Reserved for a host-supplied CSS file on top of
# Administrate's stock styling. Accepted but not applied yet.
c.theme_css_path = nil

# 4. before_admin_action — Optional callable that runs before every
Expand Down Expand Up @@ -192,18 +191,51 @@ bin/seams resolve --eject admin/app/dashboards/admin/identity_dashboard.rb
# Now edit engines/admin/app/dashboards/admin/identity_dashboard.rb freely.
```

For new dashboards covering host-specific models, generate via
Administrate, then move the result under the `Admin::*` namespace:

```bash
bin/rails generate administrate:dashboard MyHost::Project
# Move app/dashboards/my_host/project_dashboard.rb to
# engines/admin/app/dashboards/admin/project_dashboard.rb
# Move the controller similarly under app/controllers/admin/.
# Add `resources :projects, controller: "/admin/projects"` to
# engines/admin/config/routes.rb (or splice via the
# admin.routes.after_resources insertion point in a follow-up generator).
```
To add a dashboard for one of your own models, e.g. `Project`:

1. **Dashboard:** `engines/admin/app/dashboards/admin/project_dashboard.rb`.
Generate a starting point with `bin/rails generate administrate:dashboard Project`,
then move it here, rename it to `Admin::ProjectDashboard`, and declare
its model. Administrate would otherwise guess `Admin::Project`:

```ruby
module Admin
class ProjectDashboard < Administrate::BaseDashboard
def self.model
::Project
end
# ATTRIBUTE_TYPES, COLLECTION_ATTRIBUTES, ... as generated
end
end
```

2. **Controller:** `engines/admin/app/controllers/admin/projects_controller.rb`,
subclassing the gated base controller. Delete the controller
Administrate generated in your app's `app/controllers/admin/`, because
it bypasses the gate:

```ruby
module Admin
class ProjectsController < ::Seams::Admin::ApplicationController
def resource_class = ::Project
def dashboard = (@dashboard ||= ProjectDashboard.new)
end
end
```

3. **Policies:** `Admin::Platform::ProjectPolicy` and `Admin::Tenant::ProjectPolicy`
under `engines/admin/app/policies/admin/{platform,tenant}/`. Subclass the
namespace's `ApplicationPolicy`. The controller name picks the policy, and
an action with no policy is denied.

4. **Route:** add this to `engines/admin/config/routes.rb`, inside the
`scope as: :admin do` block:

```ruby
resources :projects, controller: "/admin/projects"
```

The leading slash is required. The sidebar picks the dashboard up automatically.

Administrate's full dashboard / field / controller surface is documented
at <https://administrate-demo.herokuapp.com/>.
Expand All @@ -222,8 +254,9 @@ at <https://administrate-demo.herokuapp.com/>.

- **2FA / IP allow-list for `/admin`.** Not shipped. Wire your own via
`Seams::Admin.config.before_admin_action`.
- **Branded UI.** Stock Administrate CSS only in v1. Override via
`theme_css_path`.
- **Branded UI.** Stock Administrate CSS only. `theme_css_path` is
accepted but not applied yet. To restyle, override Administrate's
views (see Administrate's documentation).
- **Search beyond Administrate's stock.** Administrate ships a basic
Ransack-backed search; rich filtering / faceted search is your job.
- **Batch actions.** Not in Administrate's surface; not shipped here.
Expand Down
Loading
Loading