The backend for Orbit -- an AI-powered habit tracker. Provides a REST API for habit management, an AI agent (chat + MCP tools), gamification, goals, calendar sync, social/accountability, push notifications, and subscription billing. Built with Clean Architecture and CQRS.
Live: api.useorbit.org | App: app.useorbit.org | Landing: useorbit.org
| Layer | Technology |
|---|---|
| Runtime | .NET 10.0, C# 13 |
| Database | PostgreSQL via EF Core 10 (Npgsql) |
| CQRS | MediatR 14 |
| Validation | FluentValidation 12 |
| Auth | JWT Bearer + email verification codes + Google OAuth (Supabase tokens) |
| AI | OpenAI — gpt-4.1-mini (primary), gpt-5.4-nano (sub-tasks), via the OpenAI .NET SDK |
| Resend (verification codes, contacts) | |
| Push | Firebase Admin SDK (FCM, native Android) + WebPush (VAPID, browsers) |
| Payments | Stripe (web) + Google Play Billing (native Android) |
| Storage | Supabase object storage (uploads bucket) |
| Password/Token Hashing | BCrypt |
| Observability | Sentry |
| API Docs | Scalar (dev only) |
| Testing | xUnit + FluentAssertions |
| Containerization | Docker (multi-stage build) |
- Habit CRUD -- Create, update, delete, duplicate, reorder habits with sub-habit support
- Smart Scheduling -- Server-side frequency calculations (daily, weekly, monthly, yearly, every N days) with day-of-week filtering and overdue detection
- Completion Logging -- Toggle completion per day with optional notes, full log history
- Metrics -- Current/longest streaks, weekly/monthly completion rates
- AI Agent -- Multi-turn OpenAI-backed chat that creates habits, logs completions, analyzes progress, suggests breakdowns, assigns tags, and reschedules. Exposed to external clients as MCP tools with per-tool ownership/policy scoping and audit logging. Supports image upload
- AI Assists -- Cached daily summaries, retrospectives, fact extraction, goal review, habit/tag/reschedule suggestions, proactive check-ins, slip alerts, and batched usage summaries
- Gamification -- XP, levels, streaks, streak freezes, and achievements
- Goals -- Goal tracking with deadlines and AI-assisted review
- Calendar -- Google Calendar auto-sync of scheduled habits
- Social & Accountability -- Friends, public profiles, accountability partners, challenges, and referrals
- Checklist Templates -- Reusable habit/checklist templates
- Tags -- Colored tags for habit organization
- User Facts -- Personal context facts that enhance AI responses (soft-deleted)
- Push Notifications -- Dual delivery: FCM for native Android, VAPID Web Push for browsers, plus background schedulers for reminders, goal deadlines, slip alerts, and check-ins
- Email Verification -- Passwordless code-based login via Resend
- Subscription Billing -- Stripe (web) and Google Play Billing (native) with webhook / RTDN processing; backend is the source of truth for entitlements
- Waitlist -- Signed-token waitlist confirmation flow
- Sync -- Batched pull/mutation sync endpoints for clients
- Timezone Support -- All user-facing dates use timezone-aware "today" based on user profile
- Pagination -- List endpoints paginated with
PaginatedResponse<T>
- .NET 10.0 SDK
- PostgreSQL (or Supabase for hosted)
- Docker (optional, for containerized deployment)
# Clone the repository
git clone https://github.com/thomasluizon/orbit-api.git
cd orbit-api
# Copy environment config
cp .env.example .env
# Edit .env / appsettings.Development.json with your database credentials and API keys
# Apply database migrations
dotnet ef database update --project src/Orbit.Infrastructure --startup-project src/Orbit.Api
# Run the API
dotnet run --project src/Orbit.ApiThe API runs at http://localhost:5000. API docs available at /scalar in development.
Settings are bound from appsettings.json, with secrets overridden in appsettings.Development.json locally and environment variables in production. Key sections:
| Section | Purpose |
|---|---|
ConnectionStrings:DefaultConnection / SessionConnection |
PostgreSQL (direct + session pooler) |
AI:ApiKey / AI:Model / AI:SubTaskModel / AI:BaseUrl |
OpenAI credentials and models (BaseUrl defaults to https://api.openai.com/v1) |
Jwt:SecretKey / Issuer / Audience |
JWT signing and claims |
Supabase:Url / AnonKey / SecretKey / Bucket |
Supabase auth tokens + object storage |
Resend:ApiKey / FromEmail |
Transactional email |
Vapid:PublicKey / PrivateKey / Subject |
Web Push (VAPID) |
Encryption:Key |
At-rest field encryption |
Google:ClientId / ClientSecret |
Google OAuth + Calendar |
Stripe:SecretKey / WebhookSecret / price IDs |
Stripe billing |
Cors:AllowedOrigins / LandingOrigins |
Allowed frontend origins |
Sentry:Dsn / Environment |
Error monitoring |
Clean Architecture with four layers:
src/
Orbit.Api/ # Presentation layer
Controllers/ # 26 controllers (see below)
Extensions/ # PayGate-aware IActionResult helpers
Middleware/ # Security headers, unhandled/validation exception handlers
Program.cs # DI configuration, middleware pipeline
Orbit.Application/ # Application layer (CQRS)
Common/ # PaginatedResponse<T>, ErrorMessages, AppConstants, PayGateService, ...
Habits/ # Commands / Queries / Validators / Services (HabitScheduleService)
Chat/ # AI chat orchestration
Goals/ Gamification/ # Goals, XP/streaks/achievements
Calendar/ # Google Calendar sync
Accountability/ Social/ Challenges/ Referrals/ # Social graph
ChecklistTemplates/ Tags/ UserFacts/ Uploads/ # Content
Auth/ Profile/ ApiKeys/ Subscriptions/ Support/ Notifications/ Waitlist/
Behaviors/ # MediatR pipeline (ValidationBehavior, ...)
Orbit.Domain/ # Domain layer (zero dependencies)
Entities/ Enums/ Interfaces/ Common/ Models/ # Entities, Result<T>, repository/service contracts
Orbit.Infrastructure/ # Infrastructure layer
Persistence/ # OrbitDbContext, GenericRepository<T>, UnitOfWork
Services/ # AI (AiIntentService, AiSummaryService, OpenAiBatchPollerService, ...),
# Agent/MCP (AgentCatalogService, AgentOperationExecutor, AgentPolicyEvaluator, ...),
# billing (StripeBillingService, GooglePlayBillingService),
# push + schedulers (PushNotificationService, ReminderSchedulerService, ...),
# auth (JwtTokenService, GoogleTokenService), UserDateService, EncryptionService
Migrations/ # EF Core migrations
tests/
Orbit.Domain.Tests/ # xUnit + FluentAssertions
Orbit.Application.Tests/ # xUnit + FluentAssertions
Orbit.Infrastructure.Tests/ # xUnit + FluentAssertions
Accountability, Achievements, Ai, ApiKeys, Auth, Calendar, Challenges, Chat, ChecklistTemplates, Config, Friends, Gamification, Goals, Habits, Notification, OAuth, Profile, PublicProfile, Referral, Subscription, Support, Sync, Tags, Uploads, UserFacts, Waitlist.
- Result<T> -- Handlers return
Result<T>instead of throwing for expected failures - CQRS -- Command/query separation via MediatR, one folder per feature
- Factory methods -- Entities created via
Entity.Create()static methods - Generic repository + Unit of Work -- Abstracted data access
- PayGate -- Subscription-gated features via
PayGateServiceandResultpropagation helpers - Validation pipeline --
ValidationBehavior<TRequest, TResponse>in the MediatR pipeline - Cache invalidation -- AI summary cache cleared on any habit mutation
Dual delivery via PushNotificationService:
- FCM -- Firebase Admin SDK for native Android. Subscriptions with
p256dh == "fcm"route through Firebase - Web Push -- VAPID-based for browsers, via
Lib.Net.Http.WebPush - Schedulers -- Background services (
ReminderSchedulerService,GoalDeadlineNotificationService,SlipAlertSchedulerService,ProactiveCheckinSchedulerService) send push + create in-app notifications; aSentReminderrecord prevents duplicates
# Run all unit tests
dotnet testTests use xUnit with FluentAssertions. Unit tests only — there is no integration or E2E suite.
| Component | Service |
|---|---|
| Hosting | Render (Docker, manual API releases) |
| Database | Supabase PostgreSQL (session pooler) |
| Domain | api.useorbit.org |
| Push | Firebase project orbit-11d4a (FCM) |
| Resend | |
| Payments | Stripe + Google Play Billing |
| Monitoring | Sentry |
Run release.yml from main with an environment of production or staging and a branch to release. Production accepts only main; staging deploys the selected branch head. Both jobs use the Render API to wait for the requested deploy and verify the live commit. Staging also verifies the commit reported by the public health endpoint. Set RENDER_API_KEY as an environment secret in each of the production and staging GitHub environments, and set the repository variables RENDER_PRODUCTION_SERVICE_ID and RENDER_STAGING_SERVICE_ID. Restrict the production, staging, and render-operations GitHub environments to deployment branch main. The staging reseed and Postgres access reconciliation workflows use the render-operations environment secret RENDER_API_KEY for database and service management. Remove the repository secret RENDER_API_KEY after both release paths and the operational workflows succeed.
Turnstile remains disabled after deployment. Create a Turnstile widget for useorbit.org in the Cloudflare dashboard and set BotProtection__SecretKey in the Render orbit-api environment. Keep BotProtection__Enabled unset or false until all three conditions hold:
- The deployed web sign-in sends
turnstileTokenfor both send-code and verify-code. Check production requests and the deployed web version. CheckAPP_VERSIONin every deployed environment of the Orbit web project in Vercel before raising the version floor. An unsetAPP_VERSIONfails open; a set value must be at least the proposed floor. - The Android build sends
turnstileTokenfor both calls and is live on the Google Play track. Check the Play Console release and active version distribution. Only then raise the productionAppConfigs.MinSupportedVersionrow to that build using a guarded update, and read the row back to verify the exact value. Allow up to 30 minutes for the per-process cache to apply the floor. - The deployed landing waitlist sends
turnstileTokenin the/api/waitlistbody. The source atorbit-landing-page/src/scripts/waitlist.tsalready does this onmain; check the deployed form request and confirmPUBLIC_TURNSTILE_SITE_KEYis set for the landing deployment.
Then set BotProtection__Enabled=true in the Render orbit-api environment. Verify token-bearing web and Android sign-in and the landing waitlist succeed, while tokenless requests to all five protected routes return 400. Monitor HTTP 426 traffic and client reports for 24 hours after the version-floor raise. Existing clients without a token continue to work while the switch is false. The production smoke account bypasses this gate only when both SMOKE_TEST_EMAIL and SMOKE_TEST_CODE are configured, so knowledge of that address permits sends only to Orbit's own mailbox.
# Build and run with Docker Compose
docker compose up -d --buildThe Dockerfile uses a multi-stage build (SDK for build, ASP.NET runtime for production) and runs as a non-root user.
| Repo | Description |
|---|---|
| orbit-ui-mobile | Turborepo frontend — apps/web (Next.js 16) + apps/mobile (Expo, Android) + packages/shared |
| orbit-landing-page | Marketing landing page |
Private project.