Sixth section of #141, which closes the issue's defect: start no longer waits on Temporal before it serves. Start derives the Stripe key's mode and fingerprint, sets the SDK's key, opens the database, applies the operator overrides and the boot data state, binds the listener, and only then starts the Temporal connector and the identity provider's discovery beside the server. A bind error returns before any dial or discovery request. While Temporal is unreachable the console serves: sign-in, the domain ask, the static files and every page that needs no workflow answer as usual, and the Temporal-backed actions answer as the previous section made them. The connector runs the retry loop. Each attempt dials once (NewClient no longer retries on its own; dialWithRetry and its budget are gone), builds the worker and runs its precondition. A precondition that refuses keeps the client and retries at the cap, so FedWiki's refusal over open runs of a removed workflow type is read again on the same connection; one that cannot read what it checks, a worker that fails to build or start, closes the client and dials again. The attempt ends when the worker starts: "connected to Temporal server" is logged with the attempts and the time it took, then the holder is published and the integrations' Startup hooks and the schedules run as before. "Temporal not ready yet, retrying" is a Warn until temporal-connect-timeout (90 seconds by default), whose help now says so, and an Error from then on, under the loop's decay. A Temporal OAuth configuration that cannot be built still refuses at start. start exits 1 when serving fails, at any point, and returns only after the connector and discovery have returned. A failure before the listener keeps the message it had (the overrides, the boot data state, the integration connections); a serving failure logs "server failed to start". The Stripe webhook activity marks an event processing only from received, processing or failed, so a run of an event already completed, skipped or dead-lettered returns at once and dispatches nothing. The handlers and drains that found no Temporal client logged that it was not configured; with Temporal still connecting that was false, so they log "Temporal is not connected" (the six plan-act goldens carry the new drain line). Copies 13, 15 and 16 also carry the host and namespace. Checked by hand on the test stack: with Temporal stopped, the console served /login and the ask at once; the connector logged Warn, then Error from the first attempt after 90 seconds; when Temporal came back the same process logged "connected to Temporal server" after 14 attempts. With the issuer refusing, /login answered the styled 503 with Retry-After: 5 and the ask answered. Full suite on reset databases: 4513 pass, 15 skip, none failed. go test -race -p 1 over cmd, auth, server, retryloop, workflows and the three integrations after reset-test-db.sh: every package passes, no data race. make lint passes. Breaks, each caught (section 1's UseKey break among them). Two depart from item 6.10's first wording, which the item now carries: the synchronous-fetch break sits between listen and the server's start (before listen the row would fail waiting for the address, not at its one-second assertions), and the "returns without waiting" break is caught by the fourth boot row, whose fake dial holds for 200 ms of wall-clock time past the context's end, the least that makes an early return observable. - serveConsole without UseKey - UseKey after the bound server's Serve - Serve without go Discover - Serve fetches the discovery document before serving - serveConsole waits for connectTemporal before Serve - connectTemporal started before Bind returns - serveConsole returns without waiting for its goroutines - serveConsole returns without waiting for its goroutines (race) - skipSession without the ask's path - connectTemporal publishes after afterConnect - connectTemporal publishes before worker.Start - connectTemporal returns after the escalation time - the open-runs refusal closes the client - ErrCannotCheck treated as a refusal - worker.Start failure leaves the client open - listRunsError without its Is method - worker.Start failure logged from inside the attempt - ProcessWebhookEvent without its status guard - the guard without failed - the guard without processing - NewClient without the deadline - connectTemporal under serveConsole's own context - the attempt runs the hooks before copy 14 - Run logs copy 21 for a step failure - the overrides failure is a plain error - a Temporal authorization that cannot be built is not refused
Temporal Workflows
This directory contains the Temporal workflow definitions, activity implementations, and worker configuration for the member-console application.
Structure: Domain-Based
We use a Domain-Based (or Feature-Based) structure. Instead of grouping all workflows in one folder and all activities in another, we group them by business domain.
internal/workflows/
├── client.go # Temporal client factory
├── worker.go # Central worker setup & registration
├── queues/
│ └── queues.go # Task Queue constants
├── common/ # Shared utilities & generic activities
│ └── options.go # Standard ActivityOptions (retries, timeouts)
├── example/ # "Example" Domain
│ ├── workflow.go # Workflow definition
│ └── activities.go # Domain-specific activities
├── fedwiki/ # "FedWiki" Domain - Site provisioning
│ ├── README.md # Domain documentation
│ ├── FARMMANAGER.md # FarmManager API documentation
│ ├── workflow.go # Create/Delete site workflows
│ ├── activities.go # Site management activities
│ └── farmmanager_client.go # HTTP client for FarmManager API
└── onboarding/ # (Future) "Onboarding" Domain
├── workflow.go
└── activities.go
Adding a New Domain
- Create a directory:
internal/workflows/<domain_name>/ - Define the Workflow: Create
workflow.goin that directory. - Define Activities: Create
activities.goin that directory.- Define a struct (e.g.,
Activities) to hold dependencies (DB, Logger). - Implement a
NewActivitiesconstructor.
- Define a struct (e.g.,
- Register: Go to
internal/workflows/worker.goand add your new domain to theNewWorkerfunction:
// internal/workflows/worker.go
// ... inside NewWorker ...
w.RegisterWorkflow(yourdomain.Workflow)
w.RegisterActivity(yourdomain.NewActivities(cfg.DB, cfg.Logger))
Usage
Starting the Worker
The worker is embedded in the main application but can be extracted later.
import "git.coopcloud.tech/wiki-cafe/member-console/internal/workflows"
// ... in your main setup ...
workerCfg := workflows.DefaultWorkerConfig(database, logger)
worker, err := workflows.NewWorker(temporalClient, workerCfg)
if err := worker.Start(); err != nil {
// handle error
}
defer worker.Stop()
Starting a Workflow
import (
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows"
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/queues"
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows/example"
)
// ... inside an HTTP handler ...
workflowOpts := client.StartWorkflowOptions{
ID: "example-" + uuid.New().String(),
TaskQueue: queues.Main,
}
we, err := temporalClient.ExecuteWorkflow(
ctx,
workflowOpts,
example.Workflow, // Pass the function reference
example.Input{Message: "hello"},
)