Skip to content

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/grpc go 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, when PULSE_WEB_DIR is 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:

  1. Panic recovery.
  2. Optional per-caller rate limiting, keyed by peer IP. It is off unless PULSE_RATE_RPS is set.
  3. Request logging.
  4. 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 with license 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.

AreaServices
AnalysisDiagnosticService, RadarService, InsightsService, Customer360Service
ContentContentService, StudioService, AvatarService, CourseService, PublishingService
Campaigns and resultsCampaignService, MetricsService, AnalyticsService, FollowUpService
Leads and accountsLeadService, IntegrationService, InfluencerService
AIModelService, TwinService, ActivityService
PlatformIdentityService, ConfigService, SelfHealService, MeteringService (deprecated)
Shared ui componentsBillingService, 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.

FindingBecomes
The 4-week plan (cronogram)A 30-day follow-up cycle with one planner task per entry
Channel prescription suggestionsDraft content, pending review, on the Approval screen
Prescriptions and audience segmentsOne draft campaign linked to the diagnosis
Audience segmentsOne Customer 360 profile per segment
Competitive and positioning analysisA 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:

WorkerWhat it does
Social engineDaily. 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 workerEvery 30 seconds. Dispatches scheduled posts that are due.
Podcast, audiobook, and shorts enginesGenerate episodes and videos for active campaigns and stage them. The shorts engine refuses to start when the image has no video tool.
Document engineDaily. Generates documents for active campaigns.
Predictive churn engineDaily. Scores synced HubSpot leads for churn risk and schedules retention tasks.
Site audit engineDaily at 03:47 UTC. Audits one configured site and files a digest. See Site pipeline.
Self-healing loopWatches 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 integrations table 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.
The Litestream replica is plaintext at rest, including password hashes. The Litestream version in use cannot encrypt it, and the fix is an open owner decision. See the comments in grpc/litestream.yml.