Concepts
This section explains how PULSE is put together. Read it before you change a deployment or build against the API.
One process, one port
pulse-server serve is one Go process listening on one port. A multiplexer
(cmux) splits that port in two:
- Connections with
content-type: application/grpcgo to the gRPC server. - Everything else goes to an HTTP handler. It serves gRPC-Web for browsers,
/health, generated media, the voice endpoints, sign-in helpers, the podcast feed, the A2A agent card, and, whenPULSE_WEB_DIRis set, the Flutter web build itself.
When the backend serves the web build, the app and the API share an origin and browsers never make CORS requests.
Every call goes through the same chain
gRPC calls pass through the same interceptors in the same order:
- Panic recovery.
- Optional per-caller rate limiting, keyed by peer IP. It is off unless
PULSE_RATE_RPSis set. - Request logging.
- Authentication. Every method needs a valid bearer token, except
IdentityService/Login,ModelService/GetModelStatus, and reflection. When a licence file is configured and invalid or expired, calls are refused withlicense expired or invalid.
Streaming calls also get serialized sends and a heartbeat. The heartbeat keeps long generations alive through proxies while the model is still thinking.
The services
The backend registers these gRPC services. Each one is documented in the API reference.
| Area | Services |
|---|---|
| Analysis | DiagnosticService, RadarService, InsightsService, Customer360Service |
| Content | ContentService, StudioService, AvatarService, CourseService, PublishingService |
| Campaigns and results | CampaignService, MetricsService, AnalyticsService, FollowUpService |
| Leads and accounts | LeadService, IntegrationService, InfluencerService |
| AI | ModelService, TwinService, ActivityService |
| Platform | IdentityService, ConfigService, SelfHealService, MeteringService (deprecated) |
Shared ui components | BillingService, ConnectionsService, RunnersService, ProfileService, AccessService |
A diagnosis fills the rest of the app
When GenerateDiagnostic finishes, PULSE copies the structured findings into
the tables the other screens read. This step makes no extra model calls and
invents nothing. It runs once per diagnosis.
| Finding | Becomes |
|---|---|
| The 4-week plan (cronogram) | A 30-day follow-up cycle with one planner task per entry |
| Channel prescription suggestions | Draft content, pending review, on the Approval screen |
| Prescriptions and audience segments | One draft campaign linked to the diagnosis |
| Audience segments | One Customer 360 profile per segment |
| Competitive and positioning analysis | A market summary on the Radar |
In the background, PULSE also runs market research for the niche and prospects the public web for up to five leads.
Background workers
serve starts these loops alongside the API:
| Worker | What it does |
|---|---|
| Social engine | Daily. Drafts LinkedIn, X, and Instagram copy for each active campaign, for review. When the blog channel is armed, it also drafts and schedules one blog post per campaign. |
| Publishing worker | Every 30 seconds. Dispatches scheduled posts that are due. |
| Podcast, audiobook, and shorts engines | Generate episodes and videos for active campaigns and stage them. The shorts engine refuses to start when the image has no video tool. |
| Document engine | Daily. Generates documents for active campaigns. |
| Predictive churn engine | Daily. Scores synced HubSpot leads for churn risk and schedules retention tasks. |
| Site audit engine | Daily at 03:47 UTC. Audits one configured site and files a digest. See Site pipeline. |
| Self-healing loop | Watches local resources and the inference endpoints. Read it with SelfHealService/GetStatus. |
Nothing a worker drafts is published unless the channel is armed. See Publishing controls.
Where data lives
- SQLite holds the business data: diagnostics, content, campaigns, leads, and more. In a cluster, Litestream replicates it to the in-cluster object store. SQLite has one writer, so a deployment runs one backend instance.
- OAuth tokens in the
integrationstable are sealed column by column before they are written. The rest of the database file is not encrypted. - The appfs root holds sessions, generated media, and the memo cache, each sealed. It uses the object store when one is configured, or a sealed local directory otherwise. The server will not start if it cannot mount it.
grpc/litestream.yml.