Nocturnal Post — Self-Hosted Setup Walkthrough

From zero to your first briefing on your own server. This guide is for self-hosting — you run the Docker stack and control the data. If you're a hosted (SaaS) client, use the Getting Started guide instead: no servers, just sign in.


Part 1 — Deploy the stack

  1. Get the source and start the stack:
   git clone <repo-url> nocturnalpost && cd nocturnalpost
   cp .env.example .env          # then edit .env — see below
   docker compose up -d
  1. Required .env values:
   APP_BASE_URL=https://your-domain.com     # where you'll reach it
   SECRET_KEY=<32+ random chars>            # python -c "import secrets; print(secrets.token_urlsafe(48))"
   MICROSOFT_CLIENT_ID=<from Entra>
   MICROSOFT_CLIENT_SECRET=<from Entra>
  1. One-time Microsoft Entra ID setup (app registration):
  • portal.azure.com → Microsoft Entra ID → App registrations → New registration
  • Supported accounts: Accounts in any organizational directory (multi-tenant)
  • Redirect URI (Web): https://your-domain.com/auth/callback
  • Certificates & secrets → New client secret → copy the value into .env
  • API permissions (delegated): Mail.Read, Mail.Send,

ChannelMessage.Send, plus offline_access/openid/profile

  • Admin consent for the permissions (tenant admin action)
  • Branding → publisher domain: verify your-domain.com (serves

/.well-known/microsoft-identity-association.json automatically)

  1. Set the first-admin credentials in .env:

ADMIN_USERNAME=you@your-domain.com ADMIN_PASSWORD=<a strong password>

Open https://your-domain.com/login and log in with those credentials — the first successful login provisions the platform admin automatically (single-shot: once any admin exists the env credentials stop auto-provisioning, so change the password right after your first login). Then add your Microsoft credentials under Admin Settings if you skipped .env, and connect a mailbox under Integrations.

Users: each teammate signs in with Microsoft and is placed in the
tenant that owns their email domain. Tenant isolation is enforced
everywhere — users in different tenants can never see each other's data
or trigger each other's syncs.

Part 2 — Choose folders (1 minute)

When you connect a mailbox, Nocturnal Post discovers your top-level folders and auto-selects Inbox. If your mailbox has no folder named Inbox (IMAP migrations, heavy rules), it picks the closest sensible default — you can change it any time:

  • Mailboxes page → your mailbox → Folders
  • Toggle which folders to watch. Most teams: Inbox only. Watching

Sent or Archive doubles the noise with little value.

Each sync uses Graph delta queries — only new/changed messages transfer, and each message stores: subject, sender, recipients, timestamps, and the first ~500 characters of body (enough to classify and search, never the full mailbox).


Part 3 — Classification and work items (automatic)

Every new message is classified on arrival:

Category Meaning Becomes
urgent time-sensitive language, high importance Work item + high priority
follow_up asks you to do something Work item
needs_response a reply is expected Work item
internal from your own domain noted only
external from outside your domain noted only
automated noreply/newsletters/receipts skipped — never an item

Actionable mail becomes a work item ("Follow up: <subject>") that appears on the Items page and feeds the briefings. Automated mail (password resets, digests) is classified but never creates follow-up work.


Part 4 — Rules (Lantern) — real-time notifications

Briefings are scheduled; rules fire immediately when a matching message arrives. Set them up on the Rules page:

Rule = condition + action

Condition Matches Example value
category classified category urgent
sender_domain sender ends with @value bigclient.com
subject_contains subject contains text (case-insensitive) invoice
Action Delivery
notify_email email to the target (or the first mailbox owner if empty)
notify_teams post to your configured Teams channel

Teams channel setup

On the Rules page, under Teams Delivery Channel, paste any of:

  • The full channel URL from Teams (Channel → ⋯ → Get link to channel)

— recommended, it's one copy-paste: https://teams.cloud.microsoft/l/channel/19%3A...%40thread.tacv2/Name?groupId=...

  • JSON: {"team_id": "<groupId from the URL>", "channel_id": "19:..."}
  • Shorthand: team_id:channel_id

The URL is parsed server-side; you never need to decode it yourself.

Permission note: the connected user needs to be able to post in that
channel (typically: add the app-user to the team, or use a channel where
they're a member).

Use cases worth setting up on day one

  1. VIP client escalationsender_domain = your biggest client's

domain, action notify_teams. Their emails ping the channel instantly instead of waiting for the morning brief.

  1. Urgent anythingcategory = urgent, action notify_email.

A safety net that catches high-importance mail even outside briefings.

  1. Billing keywordssubject_contains = invoice, action

notify_email to your AP person. Nothing ages past payment terms again.

  1. Regulator/watchdog senderssender_domain = the regulator's

domain, notify_teams. Compliance-critical mail is never "missed the briefing."


Part 5 — Briefings (Dawn, Dusk, Moon)

Scheduled per tenant (America/New_York default, weekdays):

Brief Time Contents
Dawn (morning) 08:00 Overdue follow-ups · due today · emails needing response (last 24h) · high-priority mail · failed notifications
Dusk (evening) 16:30 Emails processed today · new work items · completed items · open follow-ups · tomorrow's due
Moon (weekly) Fri 08:00 Week stats · created/completed · unresolved · overdue

Delivery: queued as email to each tenant user, and posted to the Teams channel if configured. The Briefings page can send a test briefing of any type on demand (Preview / Deliver now) — use it right after setup to confirm delivery before the first scheduled run.

The daily cycle: morning brief tells you what needs attention; you work the Items list; evening brief confirms what got processed and what carries overnight.


Part 4.5 — Enable Google sign-in (optional)

To let teammates sign in with Google (gmail.com gets a one-of-one tenant per user; Gsuite domains share the company tenant):

  1. Google Cloud Console → <https://console.cloud.google.com> →

create (or pick) a project.

  1. APIs & Services → OAuth consent screen: External, fill the app

name + support email; add scopes openid, email, profile.

  1. Credentials → Create credentials → OAuth client ID

Web application:

  • Authorized JavaScript origins: https://your-domain.com
  • Authorized redirect URI:

https://your-domain.com/auth/google/callback

  1. Copy the Client ID and Client secret.
  2. In Nocturnal Post: Admin Settings → Google card → paste both →

set redirect URI https://your-domain.com/auth/google/callback → check Enabled → save.

The login page gains a Sign in with Google button immediately. Google-hosted Gmail IMAP sync additionally needs the Gmail scope (granted when a user connects their mailbox; the sign-in flow only requests identity scopes).

Part 5.5 — Mail security (header analysis + impersonation)

Nocturnal Post reads the Authentication-Results header (SPF/DKIM/DMARC verdicts your mail platform already computes) on every synced message and flags suspicious mail automatically:

  • Authentication verdictsspf_fail, dkim_fail, dmarc_fail,

dmarc_reject appear as badges on the Messages page and in the Dawn Brief's "Mail Security" section.

  • Lookalike senders — senders mimicking your domain (paypal.io vs

paypal.com, Cyrillic lookalikes, one-character typos) are flagged; high-confidence matches become a "Suspected impersonation of \<your domain\>" work item.

  • BEC heuristics — executive display-name impersonation, urgent

financial language from first-time senders, gift-card/crypto requests.

Nothing to configure — the flags appear as your mail flows through. The Admin Dashboard (/admin/dashboard, platform admin) shows the same security picture across all tenants plus per-tenant CSV service reports.

Part 6 — Retention, search, and the data lifecycle

  • The Vault (hourly): deletes messages, previews, and outbound logs

past the tenant's retention window. Work items are kept by default (they're your to-do list, not email data). Default maximum: 30 days.

  • Search page: semantic search over your live window — ask by meaning

("who mentioned the contract redline?"), not keywords. Requires the AI_EMBEDDINGS_* settings (see readme); vectors are purged with the same retention rules.

  • Disconnect (Integrations page): revokes your OAuth tokens at

Microsoft, deletes them locally, deactivates your mailbox. Sync stops immediately.

  • Export / delete: tenant admins can export all data as JSON or wipe

email-derived data entirely (platform admin, confirm required).


Troubleshooting

Symptom Cause / fix
No folders discovered Token expired or Graph permissions missing — reconnect at Integrations
"No Teams channel configured" in briefings Add a channel on the Rules page (see Part 4)
Teams send fails with config error Re-paste the channel URL; check the connected user can post in the channel
Mailbox synced 0 new messages repeatedly Expected on a quiet inbox — delta syncs only fetch changes
Briefing sections all empty First run populates after a sync; check Messages page has rows
Briefing email not arriving Check the Outbound page for send status and error messages
Discovery keeps re-running in logs Older build issue — folder fallback fixed in 0.7.x; update

Questions about anything on this page? Contact us — we answer everything.