制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗A modern, high-performance diabetes management platform built with .NET 10. Nocturne is a complete rewrite of the Nightscout API with full feature parity, providing native C# implementations of all endpoints with optimized performance and modern cloud-native architecture.
Nocturne is a comprehensive diabetes data platform that provides:
Nocturne/
├── src/
│ ├── API/ # REST API (Nightscout-compatible)
│ ├── Aspire/ # Aspire orchestration
│ ├── Connectors/ # Data source integrations (Dexcom, Libre, etc.)
│ ├── Core/ # Domain models, interfaces, and constants
│ ├── Desktop/ # Desktop application
│ ├── Infrastructure/ # EF Core data access, caching, security
│ ├── Portal/ # Marketing website
│ ├── Services/ # Background services
│ ├── Tools/ # CLI tools and MCP server
│ ├── Web/ # pnpm monorepo (SvelteKit frontend, bot, bridge)
│ └── Widgets/ # Embeddable widgets
└── tests/ # Comprehensive test suite
crates/). Without it, the
build downloads the engine library that main published for your checkout's crates/.Aspire orchestrates all services with a single command:
aspire start
Aspire will automatically:
crates/nocturne-alerts-ffi) when Rust is installedOnce running, access the app at https://nocturne.localhost:1612. On an instance with no tenants (a fresh clone, and every worktree, whose Postgres is ephemeral), the AppHost seeds a dev tenant with 30 days of sample data before the web app starts, at https://dev.nocturne.localhost:1612, and dev auto-login signs you in as its owner, so the first page is a populated dashboard. aspire logs nocturne-dev-seed shows what the seed did and how long it took. An instance that already holds a tenant is never seeded, so an existing database keeps its own tenants and has no dev. A first start stopped during the seed leaves a partial dev tenant that is not seeded again: delete it (the "Delete Tenants" command on nocturne-postgres-server) and restart. aspire describe lists the services and aspire logs <resource> tails one. The Aspire dashboard is off by default; start with Aspire__OptionalServices__AspireDashboard__Enabled=true aspire start (or set Aspire:OptionalServices:AspireDashboard:Enabled in the AppHost appsettings) to get it.
If .NET is installed outside the default location (e.g. Homebrew), set DOTNET_ROOT to the directory holding the real dotnet binary before aspire start (Homebrew: export DOTNET_ROOT="$(brew --prefix dotnet)/libexec").
In run mode the AppHost pins two host ports (main checkout; worktrees stay dynamic):
https://nocturne.localhost:1612 — the YARP gateway (the app; tenants at https://<slug>.nocturne.localhost:1612, the seeded one at https://dev.nocturne.localhost:1612)http://localhost:1610 — nocturne-api directly (stable target for scripts and the dev-only admin API)The API always runs with ASPNETCORE_ENVIRONMENT=Development under aspire start, so the dev-only surface (/api/v4/dev-only/*) exists locally and never in published images.
The dev shortcuts live under Dev in the AppHost appsettings (see appsettings.example.json). To test the first-run wizard and the passkey UI, turn both off:
Dev__AutoSeed__Enabled=false Dev__AutoLogin=false aspire start
Dev__AutoSeed__SampleDataDays sets how much history the dev tenant gets (default 30).
The default local base domain is nocturne.localhost. Browsers resolve every *.localhost name to loopback themselves, so tenant subdomains (dev.nocturne.localhost) need no DNS or hosts-file setup, and WebAuthn accepts nocturne.localhost as the passkey Relying Party ID — unlike bare localhost, which browsers reject on subdomain origins as a public suffix.
TLS: if mkcert is installed (winget install FiloSottile.mkcert / brew install mkcert), the AppHost generates a trusted wildcard certificate for *.nocturne.localhost automatically — note this runs mkcert -install, which adds the mkcert root CA to your system trust store. Without mkcert, the gateway falls back to the ASP.NET dev certificate and tenant subdomains show a browser certificate warning (functional, just noisy).
Migrating from an older checkout: the local base domain used to be bare localhost, and passkeys are scoped to the WebAuthn RP ID derived from it — credentials registered under the old domain won't assert under nocturne.localhost. Register a fresh passkey once (then export it as a fixture, below).
The dev tenant already exists (see above), so another tenant needs another slug:
curl -X POST http://localhost:1610/api/v4/dev-only/admin/seed-tenant \
-H "Content-Type: application/json" \
-d '{ "slug": "test", "displayName": "Test", "ownerUsername": "dev", "sampleData": true }'
The response includes url and loginLink — open the loginLink in a browser and you're signed in as the owner, looking at realistic CGM data. GET /api/v4/dev-only/auth/login?tenant=<slug>&format=json (or POST) returns a token pair for headless clients instead. scripts/dev-smoke.cs runs the whole path end to end: dotnet run scripts/dev-smoke.cs.
Other one-call dev endpoints: POST /api/v4/dev-only/admin/tenants/{id}/seed-sample-data (populate an existing tenant) and POST /api/v4/dev-only/admin/tenants/{id}/recovery-mode (orphan a member's credentials to exercise the break-glass flow).
Register a passkey once (any tenant's normal setup/login flow), then export it as a committed fixture:
curl http://localhost:1610/api/v4/dev-only/auth/passkey-fixture > docs/seed/dev-identities.json
WebAuthn public keys are not secret, and subjects are global rather than tenant-scoped, so the fixture is safe to commit. On every Development startup the API re-inserts the fixture's subjects and credentials (surviving DB wipes), and seed-tenant adds each fixture subject as an owner of new tenants — your authenticator signs in to all of them. The fixture only works while the base domain (and thus the RP ID) stays the same, which the nocturne.localhost default guarantees.
To use a dedicated domain on port 443 instead:
cd src/Aspire/Nocturne.Aspire.Host
dotnet user-secrets set "LocalDev:Domain" "nocturne.test"
This requires mkcert and one hosts-file line per tenant slug (127.0.0.1 dev.nocturne.test — hosts files don't support wildcards). The app is then at https://nocturne.test with no port.
The easiest way to deploy Nocturne is with the production Docker Compose bundle. Each GitHub Release includes ready-to-use artifacts, or you can generate them locally.
Download docker-compose.yaml and .env.example from the latest release.
# 1. Copy the env template and fill in your passwords and domain
cp .env.example .env
# 2. Start Nocturne
docker compose up -d
The bundled Caddy is the hop that decides which address Nocturne records in audit rows and counts
rate limits against, and by default that is whoever connected to it. Behind a CDN that is the CDN's
edge, so every visitor sharing a point of presence shares one bucket. Set TRUSTED_PROXIES to the
CDN's published ranges (Cloudflare's are at https://www.cloudflare.com/ips/) and Caddy will take
the visitor's address from CF-Connecting-IP, or from the last entry of X-Forwarded-For that no
declared proxy vouched for — but only from those ranges, so nobody else can claim to be someone
they aren't. This works for any CDN, not just Cloudflare. Leave it alone if no CDN is in front, and
never set it empty: an empty value is a Caddy startup error, not a way to turn the feature off.
The production compose includes Watchtower for automatic container updates (checks daily), and omits the Aspire dashboard and Scalar API explorer. Watchtower will automatically pull new images as they are published. It runs in label-only mode and every service in the bundle carries a com.centurylinklabs.watchtower.enable label, so it only ever updates Nocturne's own containers and leaves anything else on the same Docker host alone. Caddy and Postgres carry false: they take their config from Compose, which Watchtower does not carry across a recreate. Update them with docker compose pull && docker compose up -d. An install from before this change still has true on caddy: set the label to false there, or download the new bundle, then run docker compose up -d to recreate it.
A fresh install has no tenants, so the API answers every request with 503 {"error":"setup_required"} until the first owner secures the instance. This is expected — it's the signal for the web UI to show the setup wizard. A local aspire start skips this step: it seeds a dev tenant (see Quick Start with Aspire).
In a browser (normal path). Open your Nocturne site (the apex https://<BASE_DOMAIN>/). The setup wizard walks you through creating the first tenant and registering a passkey (or linking an OIDC provider). When it completes you're shown a set of recovery codes — save them. That's the whole setup.
Headless / automated setup (no browser). Trusted automation can stand up a tenant — create it, configure connectors, change settings, push data — before any human registers a passkey, using the INSTANCE_KEY from your .env. The instance key is the highest-trust service credential (cross-tenant platform admin), so treat it like a root password. Requests authenticate with two headers: X-Instance-Key carrying the SHA-256 hex of INSTANCE_KEY, and an X-Instance-Service marker naming the caller.
# The X-Instance-Key header is the SHA-256 hex of your raw INSTANCE_KEY, not the key itself.
KEY_HASH=$(printf %s "$INSTANCE_KEY" | sha256sum | cut -d' ' -f1)
# 1. Create the first tenant. This endpoint is anonymous and tenantless — call it on the apex.
curl -fsS -X POST "https://$BASE_DOMAIN/api/v4/setup/tenant" \
-H 'Content-Type: application/json' \
-d '{"slug":"alice","displayName":"Alice"}'
# 2. Configure the tenant as trusted automation. Target the tenant's subdomain and
# present the instance key + service marker. Normal tenant traffic still gets 503
# until a human completes setup, but instance-key calls are allowed through.
curl -fsS -X PUT "https://alice.$BASE_DOMAIN/api/v4/connectors/config/Dexcom/secrets" \
-H "X-Instance-Key: $KEY_HASH" \
-H 'X-Instance-Service: nocturne-setup-agent' \
-H 'Content-Type: application/json' \
-d '{ "username": "...", "password": "..." }'
When the human is ready, they open the site and register the first passkey through the normal wizard — recovery codes and all — exactly as on a brand-new instance. Their pre-configured connectors and data are already there. (A bare X-Instance-Key without the X-Instance-Service marker is ignored, so an instance key accidentally forwarded onto a browser request can't bypass setup.)
Redirected to /setup even though a tenant is already configured. Two usual causes:
BASE_DOMAIN is unset. The API resolves tenants as {slug}.{BASE_DOMAIN}, and the WebSocket bridge requires it. If it's empty, host→tenant resolution fails and the dashboard bounces to /setup (and the bridge logs BASE_DOMAIN is required). Set it in .env to the domain you serve Nocturne on, for both the API and web containers, then recreate them.https://<BASE_DOMAIN>/, no subdomain), update to the latest image — older builds reported setup_required for /api/v4/status on the apex and bounced configured single-tenant installs to /setup.Redirected to /auth/recovery. A member of the tenant has no way left to sign in (no passkey, and no working sign-in provider), so the tenant is in recovery mode until they register a passkey. The member can sign in with one of their own recovery codes. Otherwise someone has to issue them a one-time recovery code: an administrator who can still sign in sees the locked-out members on the /auth/recovery page and can create a link there. If nobody who can sign in has that authority (the only owner is the one locked out, or the account is an instance administrator or also belongs to another tenant), the operator issues the code with the instance key:
KEY_HASH=$(printf %s "$INSTANCE_KEY" | sha256sum | cut -d' ' -f1)
H=(-H "X-Instance-Key: $KEY_HASH" -H 'X-Instance-Service: nocturne-operator')
curl -fsS "${H[@]}" "https://$BASE_DOMAIN/api/v4/admin/tenants" # find the tenant id
curl -fsS "${H[@]}" "https://$BASE_DOMAIN/api/v4/admin/tenants/<tenant-id>" # find the member's subjectId
curl -fsS "${H[@]}" -X POST \
"https://$BASE_DOMAIN/api/v4/admin/tenants/<tenant-id>/members/<subject-id>/recovery-code"
Send the member https://<tenant-host>/auth/recovery?code=<code>. The code works once and expires after 24 hours. Using it cancels any recovery codes the member still had.
If you have the .NET 10 SDK and Aspire CLI installed, you can generate the production bundle from source:
dotnet run scripts/publish-release.cs # outputs to ./release-output
dotnet run scripts/publish-release.cs ./deploy # or specify a directory
Nocturne uses three separate PostgreSQL roles for defense in depth. All three have NOBYPASSRLS so they obey Row Level Security policies, even when the database has no superuser connected.
| Role | Purpose | Privileges |
|---|---|---|
nocturne_migrator | Runs EF Core migrations (schema DDL). Owns the database and public schema. | CREATE, ALTER, DROP on tables. Cannot bypass RLS. |
nocturne_app | Runtime connection pool for the .NET API. Owns nothing. | SELECT, INSERT, UPDATE, DELETE on migrator-created tables. Cannot bypass RLS. |
nocturne_web | SvelteKit bot framework (chat state storage). Owns only its own chat_state_* tables. | CREATE on public schema (for its own tables only). No access to Nocturne tenant tables. Cannot bypass RLS. |
The bootstrap user (POSTGRES_USER) is only used for initial container setup. After container-init/00-init.sh runs, all application traffic flows through the three roles above. Passwords are set via environment variables in .env.
For bring-your-own PostgreSQL (not using the bundled container), run docs/postgres/bootstrap-roles.sql once as a superuser. See the comments in that file for details.
# Run every collected .NET test
dotnet test
# Run unit tests only
dotnet test --filter "Category!=Integration&Category!=Performance"
# Run the end-to-end suite (production images in docker compose; see tests/README.md)
cd e2e && pnpm install && pnpm e2e
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
# Create a new migration
dotnet ef migrations add YourMigrationName -p src/Infrastructure/Nocturne.Infrastructure.Data.Migrations -s src/API/Nocturne.API
# Apply migrations
dotnet ef database update -p src/Infrastructure/Nocturne.Infrastructure.Data.Migrations -s src/API/Nocturne.API
The API loads the migrations project at runtime and applies pending migrations on startup. dotnet watch does not see that project, so after adding a migration restart the API: aspire resource nocturne-api restart.
API documentation is available via Scalar at https://localhost:1612/scalar when running locally.
Nocturne aims to match Nightscout's API 1:1, so any Nightscout API endpoint should be usable. Nocturne-only endpoints are scoped to v4.
Both the API and web containers are instrumented with OpenTelemetry and export metrics, traces, and logs over OTLP push when OTEL_EXPORTER_OTLP_ENDPOINT is set. With no endpoint configured, telemetry is collected in-process and discarded with negligible overhead, so there is nothing to turn off on a default install. There is no Prometheus /metrics scrape endpoint — to use Prometheus/Grafana, run an OpenTelemetry Collector with an otlp receiver and a prometheus exporter.
OTEL_* variables to the nocturne-api and nocturne-web services.observability.otlp.enabled: true and observability.otlp.endpoint (lights up both containers). See the Helm chart README.Full setup guide: Observability docs.
Nocturne is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). Commercial licensing is available for organizations that need to use Nocturne without AGPL obligations — contact the maintainers for details.
Nocturne is a community project and is not affiliated with or endorsed by the Nightscout Project, Abbott, Dexcom, Medtronic, Glooko, or MyFitnessPal.
Important: This software is provided as-is for personal use. Always verify glucose readings with approved medical devices. Never make treatment decisions based solely on data from this application.
Nocturne is a labor of love built by volunteers. If you find it useful, please consider supporting the project: