
The Memo backend is a monolithic Go application structured into 29 packages around a central App orchestrator. It serves a plain REST + SSE API over localhost:8090 using only the standard library's http.ServeMux.
// main.go
func main() {
port := flag.Int("port", 8090, "HTTP server port")
flag.Parse()
app, _ := app.New()
app.Start()
webserver.ListenAndServe(app, *port)
}
app.New() initializes all subsystems. webserver.ListenAndServe registers routes and starts the HTTP server.
internal/app/The App struct is the brains of the backend:
type App struct {
cfg *config.Config
store *memory.Store
provider *provider.Router
conductor *orchestra.Conductor
executor *agent.Executor
syncer *cloudsync.Drive
waclient *whatsapp.Client
calendar *calendar.Store
sessions *sessions.Manager
identity *identity.Manager
skills *skill.Manager
mood *mood.Engine
db *database.DB
// ...
}
Key methods on App:
| Method | File | Purpose |
|---|---|---|
Chat() |
internal/app/chat.go |
Full chat pipeline — prompt construction, memory retrieval, provider routing, streaming |
ChatStream() |
internal/app/llm.go |
Token-level SSE streaming wrapper |
RememberExchange() |
internal/app/memory.go |
Store conversation turn in RAG |
SearchMemory() |
internal/app/memory.go |
Hybrid search with RRF |
ExecuteAgent() |
internal/app/agent.go |
Tool-calling pipeline |
RunOrchestra() |
internal/app/orchestra.go |
Multi-model parallel execution |
The internal/webserver/ package uses a bridge pattern to decouple HTTP handlers from the App:
// internal/webserver/bridge.go
type AppBridge interface {
Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
ChatStream(ctx context.Context, req ChatRequest, ch chan<- SSEEvent) error
// ... ~90 method signatures
}
type FullBridge struct {
app *app.App
}
func (b *FullBridge) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
return b.app.Chat(ctx, req)
}
Handlers only know about the bridge interface — never the concrete App. This enables testing handlers with mock bridges and future architectural changes without touching HTTP code.
internal/webserver/// internal/webserver/server.go
func ListenAndServe(app *app.App, port int) error {
mux := http.NewServeMux()
bridge := &FullBridge{app: app}
registerFlutterRoutes(mux, bridge)
return http.ListenAndServe(fmt.Sprintf(":%d", port), mux)
}
| Prefix | Count | Purpose |
|---|---|---|
/api/chat |
3 | Send message, stream response, stop |
/api/memory/ |
8 | Search, remember, forget, export, import, analytics, debug, settings |
/api/providers/ |
10 | CRUD + test for each provider type |
/api/models/ |
6 | Discover, download, list, delete, HuggingFace catalog |
/api/agent/ |
4 | Execute, permissions, pipeline status |
/api/orchestra/ |
5 | Run, config, roles, status |
/api/calendar/ |
5 | CRUD events, settings, reminders |
/api/sync/ |
6 | Push, pull, status, OAuth, settings |
/api/whatsapp/ |
8 | Pair, status, messages, send, contacts, search |
/api/skills/ |
6 | List, get, install, remove, activate |
/api/config/ |
4 | Get, set, export, import |
/api/mood/ |
3 | Score, settings, toggle |
/api/proactive/ |
4 | Settings, suggestions, history, trigger |
/api/sessions/ |
4 | List, get, delete, rename |
/api/llama/ |
5 | Start, stop, status, GPU info, models |
/api/system/ |
3 | Version, health, shutdown |
Chat and agent responses use Server-Sent Events:
// internal/webserver/handlers_flutter.go
func handleChatStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, _ := w.(http.Flusher)
eventCh := make(chan SSEEvent, 100)
go bridge.ChatStream(r.Context(), req, eventCh)
for event := range eventCh {
fmt.Fprintf(w, "data: %s\n\n", event.JSON())
flusher.Flush()
}
}
Events include: token, thinking, tool_executing, tool_result, memory_retrieved, error, done.
internal/provider/The provider router manages multiple LLM backends:
// internal/provider/router.go
type Router struct {
providers map[string]Provider // keyed by provider name
order []string // priority order (fallback chain)
mu sync.RWMutex
}
type Provider interface {
Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error)
ChatStream(ctx context.Context, req ChatRequest, ch chan<- TokenEvent) error
Health(ctx context.Context) bool
}
| Provider | File | API Type |
|---|---|---|
llama.cpp |
llama.go |
Local, OpenAI-compatible |
openai |
openai.go |
Cloud, OpenAI API |
gemini |
gemini.go |
Cloud, Gemini API |
claude |
claude.go |
Cloud, Anthropic API |
grok |
grok.go |
Cloud, xAI API |
groq |
groq.go |
Cloud, Groq API |
openrouter |
openrouter.go |
Cloud, OpenRouter API |
ollama |
ollama.go |
Local, Ollama API |
func (r *Router) Chat(ctx context.Context, req ChatRequest) (*ChatResponse, error) {
for _, name := range r.order {
provider := r.providers[name]
if !provider.Enabled() {
continue
}
resp, err := provider.Chat(ctx, req)
if err == nil {
return resp, nil
}
provider.RecordFailure()
// Continue to next provider
}
return nil, ErrAllProvidersFailed
}
After 3 consecutive failures, a provider is auto-disabled. A background health-check goroutine polls disabled providers and re-enables them when they recover.
internal/memory/// internal/memory/store.go
type Store struct {
db *sql.DB
embedModel string
mu sync.RWMutex
}
func (s *Store) Search(ctx context.Context, query string, topK int) ([]Memory, error)
func (s *Store) Store(ctx context.Context, exchange Exchange) error
func (s *Store) Forget(ctx context.Context, pattern string) (int, error)
func (s *Store) Export(ctx context.Context) ([]byte, error)
func (s *Store) Import(ctx context.Context, data []byte) error
storeMu (sync.RWMutex) protects against concurrent memory re-initialization. Never read or write the store without holding the appropriate lock.
internal/agent/// internal/agent/executor.go
type Executor struct {
tools map[string]Tool
permissions *PermissionPolicy
sandbox *Sandbox
pipeline *Pipeline
}
func (e *Executor) Execute(ctx context.Context, task string, events chan<- AgentEvent) (string, error)
filepath.EvalSymlinks before path boundary checksrun_command runs via bash -c with 60s timeoutinternal/orchestra/// internal/orchestra/conductor.go
type Conductor struct {
roles map[string]Role // 8 roles
chiefModel string
}
type Role struct {
Name string // planner, frontend, backend, ...
Provider string
Model string
SystemPrompt string
Enabled bool
}
The conductor:
orchestra:role_started, orchestra:role_completed)internal/database/// internal/database/db.go
type DB struct {
Write chan func(*sql.DB) // Serialized write queue
Read *sql.DB // Concurrent reads
}
All SQLite writes go through the Write channel to prevent database is locked errors. Reads can happen concurrently on the Read handle. This is particularly important for the memory store, where embedding writes and FTS index updates must be atomic.
Startup:
config.Load() → db.Open() → provider.Init() → memory.Init()
→ agent.Init() → cloudsync.Init() → whatsapp.Init()
→ calendar.Init() → mood.Init() → webserver.ListenAndServe()
Shutdown:
webserver.Shutdown() [stop accepting new requests]
→ whatsapp.Stop() → cloudsync.Stop() → calendar.Stop()
→ mood.Stop() → memory.Close() → db.Close()
Webserver must stop first on shutdown. Stopping subsystems before the webserver causes in-flight HTTP requests to access nil/invalid resources and crash.
http.ServeMux onlyApp directlycontext.Context as first parameter — never stored in struct fields (except lifecycle goroutines)sync.RWMutex for shared state — document why if you deviatedatabase.DB.Write channelCGO_ENABLED=1 for all Go commands