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
- 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
- Required
.envvalues:
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>
- 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)
- 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
- VIP client escalation —
sender_domain= your biggest client's
domain, action notify_teams. Their emails ping the channel instantly instead of waiting for the morning brief.
- Urgent anything —
category=urgent, actionnotify_email.
A safety net that catches high-importance mail even outside briefings.
- Billing keywords —
subject_contains=invoice, action
notify_email to your AP person. Nothing ages past payment terms again.
- Regulator/watchdog senders —
sender_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):
- Google Cloud Console → <https://console.cloud.google.com> →
create (or pick) a project.
- APIs & Services → OAuth consent screen: External, fill the app
name + support email; add scopes openid, email, profile.
- 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
- Copy the Client ID and Client secret.
- 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 verdicts —
spf_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.