SafePulse is an ASP.NET Core API with a Telegram bot integration. It stores users, groups, group memberships, and invite links in MongoDB.
- .NET SDK 9
- Docker Desktop
- MongoDB, usually started through
docker compose
Start MongoDB:
docker compose up -d mongoRun the API locally:
dotnet run --launch-profile httpInstall and run the web UI:
cd web
npm install
npm run devDefault local URLs:
- API:
http://localhost:5002 - Web UI:
http://localhost:5173 - Scalar API reference:
http://localhost:5002/scalar/v1 - OpenAPI JSON:
http://localhost:5002/openapi/v1.json - MongoDB:
mongodb://localhost:27017
The app uses appsettings.Development.json for local development. If it is missing, copy appsettings.Development.json.example and add Telegram secrets if you need webhook/bot testing.
Local development defaults:
{
"Mongo": {
"ConnectionString": "mongodb://localhost:27017",
"Database": "safepulse"
},
"Api": {
"AdminKey": "dev-admin-key"
},
"App": {
"PublicBaseUrl": "http://localhost:5002"
},
"Auth": {
"Issuer": "SafePulse",
"Audience": "SafePulse.Web",
"SigningKey": "dev-only-safe-pulse-signing-key-change-for-production-12345",
"AccessTokenMinutes": 15,
"RefreshTokenDays": 30,
"BootstrapAdminTelegramIds": [
"admin-1"
],
"BootstrapAdminEmails": [
"admin@example.com"
],
"EnableDevLogin": true
}
}Docker environment variables:
$env:API_ADMIN_KEY = "dev-admin-key"
$env:APP_PUBLIC_BASE_URL = "https://your-public-domain-or-ngrok-url"
$env:AUTH_SIGNING_KEY = "replace-with-a-long-random-secret"
$env:BOOTSTRAP_ADMIN_TELEGRAM_ID = "your-telegram-user-id"
$env:BOOTSTRAP_ADMIN_EMAIL = "admin@example.com"
$env:TELEGRAM_BOT_TOKEN = "your-token"
$env:TELEGRAM_WEBHOOK_SECRET = "your-secret"
docker compose up -d --buildWhen running inside Docker, the API uses mongodb://mongo:27017. When running with dotnet run on Windows/macOS, use mongodb://localhost:27017.
App:PublicBaseUrl is the externally reachable web/backend URL used in Telegram messages. Set it to the same public URL that opens the hosted web UI, for example your ngrok URL or mini-server domain. Large Telegram group updates use this value to link users directly to the group page.
The web UI signs in with email/password and receives an app JWT. API calls use:
Authorization: Bearer {accessToken}The refresh token is stored in an HttpOnly cookie named safepulse_refresh. The browser never needs to read it directly.
Register with email/password:
POST /api/auth/register
Content-Type: application/json
{
"Email": "admin@example.com",
"UserName": "Admin User",
"Password": "change-me-123"
}Login with email/password:
POST /api/auth/login
Content-Type: application/json
{
"Email": "admin@example.com",
"Password": "change-me-123"
}Local development can use the development login endpoint when Auth:EnableDevLogin is true:
POST /api/auth/dev
Content-Type: application/json
{
"UserId": "admin-1",
"UserName": "Admin User"
}The response contains an access token:
{
"AccessToken": "...",
"AccessTokenExpiresAt": "2026-05-22T12:00:00Z",
"User": {
"Id": "admin-1",
"UserName": "Admin User"
}
}Refresh the session:
POST /api/auth/refresh
Cookie: safepulse_refresh=...Logout:
POST /api/auth/logoutAdmin endpoints require the Admin role claim. In development, emails listed in Auth:BootstrapAdminEmails receive Admin during register/login.
Telegram webhook endpoint remains anonymous because Telegram calls it directly:
POST /api/telegram/webhookTelegram bot commands and webhook remain separate from web login for the MVP.
Create a user:
POST /api/users
Authorization: Bearer {adminAccessToken}
Content-Type: application/json
{
"Id": "admin-1",
"UserName": "Admin User",
"Status": "Unknown"
}List users:
GET /api/users
Authorization: Bearer {adminAccessToken}Change your current status from the web UI:
PATCH /api/users/{userId}/status
Authorization: Bearer {accessToken}
Content-Type: application/json
{
"Status": "Safe"
}Allowed statuses are Safe, InShelter, NeedHelp, and Unknown.
List groups where the acting user is a member:
GET /api/me/groups
Authorization: Bearer {accessToken}Create a group:
POST /api/groups
Authorization: Bearer {accessToken}
Content-Type: application/json
{
"Name": "Family"
}List groups owned by the acting user:
GET /api/groups
Authorization: Bearer {accessToken}List group members:
GET /api/groups/{groupId}/users
Authorization: Bearer {accessToken}Create an invite:
POST /api/groups/{groupId}/invites
Authorization: Bearer {accessToken}
Content-Type: application/json
{
"Note": "Telegram invite"
}Resolve an invite:
GET /api/invites/{token}Accept an invite:
POST /api/invites/{token}/accept
Authorization: Bearer {accessToken}Revoke an invite:
DELETE /api/groups/{groupId}/invites/{inviteId}
Authorization: Bearer {accessToken}Group invites use opaque tokens:
https://t.me/safe_pulse_test_bot?start=join_{token}
The Telegram /start join_{token} flow joins the Telegram user to the target group if the invite is valid and not revoked.
The older /start join_{groupId} style is still accepted as a fallback for compatibility.
Build:
dotnet build --no-restoreStart MongoDB only:
docker compose up -d mongoStart API and MongoDB through Docker:
$env:AUTH_SIGNING_KEY = "replace-with-a-long-random-secret"
$env:APP_PUBLIC_BASE_URL = "https://your-public-domain-or-ngrok-url"
$env:BOOTSTRAP_ADMIN_TELEGRAM_ID = "your-telegram-user-id"
$env:BOOTSTRAP_ADMIN_EMAIL = "admin@example.com"
docker compose up -d --build apiOpen Scalar:
http://localhost:5002/scalar/v1
Build the web UI:
cd web
npm run buildThe production web build is written to backend wwwroot/. After building the web UI, start the backend and open the backend URL directly:
cd ..
dotnet run --launch-profile httphttp://localhost:5002
For ngrok, expose the backend port only:
ngrok http 5002Then set App:PublicBaseUrl or APP_PUBLIC_BASE_URL to the HTTPS forwarding URL printed by ngrok, without a trailing slash:
$env:APP_PUBLIC_BASE_URL = "https://abc123.ngrok-free.app"The first web UI lives in web/ and is a Vite React app. For production/local Docker-style hosting, run npm run build and ASP.NET Core serves the generated files from wwwroot/.
- The fixed bottom status footer has three large buttons:
Safe,In shelter, andNeed help. - On phones the status footer uses roughly 20% of the screen height for fast tapping.
- Overview shows group/member status blocks and unique-user status totals.
- Live status changes arrive through SignalR at
/hubs/status. - Groups page can create owned groups, view members, and create invite links.
- Settings page stores API URL and development-login defaults. Keep API URL empty when frontend and backend are served from the same host.
- Users, groups, and group memberships use soft delete.
- Existing Mongo records from before soft-delete fields are treated as active.
- User list reads use the native MongoDB driver for performance.
- If
dotnet buildfails becauseHeartPulse.exeis locked, stop the running local API process and build again.