skills/local/cali-coding-go-stack/SKILL.md
[Cali] Go web applications using Datastar, Templ, DaisyUI, TailwindCSS, NATS, and optional features (Fabric.js whiteboard, LiveKit+Gemini voice AI, PocketBase/SQLite). Use when: starting a new Go web project OR evolving/extending an existing one that uses this stack. Triggers for scaffolding ("new go project", "scaffold go app", "go web boilerplate"), real-time work ("datastar go", "templ project", "go sse server", "go realtime app", "hypermedia go"), and feature evolution ("add feature to go app", "refactor go handler", "add NATS", "migrate to Templ", "extend boilerplate", "add auth", "add database", "add whiteboard", "add voice AI"). Also triggers for AI/LLM with voice ("voice AI", "voice bot", "livekit gemini", "real-time voice"). For text-only AI use the goai module in features/ai/ (included in scaffold).
npx skillsauth add renatocaliari/agent-sync-public-skills cali-coding-go-stackInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Monitor at start of every session:
When v2 is released, alert the user. Today (2026-06): v1.2.2 is the latest stable.
Go boilerplate inspired by Northstar, featuring:
Standards: This skill covers stack-specific patterns only. Concurrency, linting, security, and supply chain rules are in
cali-coding-go-standards— both skills apply simultaneously on this stack. Local dev tooling (Air,.air.toml, Makefile dev target) is also defined incali-coding-go-standards; do not duplicate that policy here. These are enforced viacali-coding-go-standards, which activates simultaneously with this skill.
⚠️ Datastar v1.0.2 Required - This boilerplate uses Datastar v1.0.2 (not RC.8). See Installation below.
Activate this skill when the user wants:
| Intent | Example Prompt | |--------|----------------| | Create Go web project from scratch | "create a new go web app" | | Create new feature in a Go web project | "create a feature" | | Add real-time/SSE | "add real-time updates to my Go app" | | UI with ready-made components | "add a dashboard with Tailwind components" | | Hypermedia-driven app | "build like Datastar/HTMX style app in Go" | | Voice AI with LiveKit | "add a voice assistant to my app" | | Collaborative whiteboard | "add a collaborative whiteboard" | | Database | "add persistence with SQLite/PocketBase" |
Read this first: MANDATORY_TEMPL_USAGE.md
This project has a ZERO-TOLERANCE policy for HTML in Go source files.
.templ files for HTMLfmt.Sprintf with HTML tags%[N]s)grep -r 'fmt\.Sprintf.*<' must return emptyNot every project needs everything. Use this decision tree:
Go Web Project
├── Need UI?
│ ├── YES → DaisyUI (always included by default)
│ └── NO → Skip to "Need data?"
│
├── Need real-time?
│ ├── Simple Pub/Sub (NATS Core) → fire-and-forget messaging
│ ├── With persistence/history (JetStream) → streams + replay
│ └── Reactive frontend only (Datastar SSE) → no NATS needed
│
├── Need database?
│ ├── Simple (1 instance, local) → SQLite
│ ├── Advanced (multi-instance, auth, REST, realtime) → PocketBase
│ └── None → in-memory data or NATS KV
│
├── Need voice AI?
│ ├── YES → LiveKit + Gemini Live API
│ └── NO → Skip
│
├── Need whiteboard?
│ ├── YES → Fabric.js
│ └── NO → Skip
Before starting, confirm with the user:
## Project Configuration
- [ ] **UI**: DaisyUI + TailwindCSS (default, always recommended)
- [ ] **Real-time messaging**: NATS Core / JetStream / None
- [ ] **Database**: SQLite / PocketBase / None
- [ ] **Voice AI**: LiveKit + Gemini / None
- [ ] **Whiteboard**: Fabric.js / None
- [ ] **Module name**: `github.com/user/projectname`
- [ ] **Deploy target**: your-server.com / other / none
⚠️ Only generate this section if the user confirms a deploy target.
Ask the user: "Este projeto será deployado em produção? Onde?"
| Target | Action |
|--------|--------|
| your-server.com | Generate full CI/CD pipeline with ghcr.io + cron |
| other | Generate pipeline with placeholders ({{SERVER_HOST}}, {{IMAGE_NAME}}) |
| none | Skip this section entirely |
your-server.comGenerate these files with concrete values (no placeholders):
Branch note: The examples use
main(GitHub default). Usemasterif the project's default branch ismaster.
.github/workflows/deploy.yml:
name: Build and Publish Docker Image
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
permissions:
packages: write
contents: read
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: docker/setup-buildx-action@v3
- run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- id: version
run: |
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- uses: docker/metadata-action@v5
with:
images: ghcr.io/{{GITHUB_REPO}}
tags: |
type=ref,event=branch
type=sha
type=raw,value=latest
- uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
build-args: |
CGO_ENABLED=0
VERSION=${{ steps.version.outputs.version }}
.github/workflows/release.yml:
name: Release
on:
push:
branches: [main]
concurrency:
group: release-please
cancel-in-progress: false
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: go
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
target-branch: ${{ github.ref_name }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Auto-merge Release PR
if: ${{ steps.release.outputs.release_created != 'true' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER="${{ fromJSON(steps.release.outputs.pr).number }}"
if [ -n "$PR_NUMBER" ]; then
gh pr merge --merge "$PR_NUMBER"
fi
Also create release-please-config.json:
{
"packages": {
".": {
"release-type": "go"
}
}
}
And .release-please-manifest.json:
{
".": "0.0.0"
}
Important: release-please requires conventional commits format. Agents must use:
fix: descricao for patch bumpsfeat: descricao for minor bumpschore:, docs:, etc. for no bump:bug: fix: — release-please ignores these⚠️ Critical prs_created trap: The release-please-action outputs prs_created as a boolean (true/false), not a PR number. Using ${{ steps.release.outputs.prs_created }} in shell will expand to <<< "true" and break. Always use steps.release.outputs.pr (actual PR number) instead.
update.sh (on server at /opt/{{APP_NAME}}/update.sh):
#!/bin/bash
set -e
IMAGE="ghcr.io/{{GITHUB_REPO}}"
CONTAINER_NAME="{{APP_NAME}}"
TOKEN_FILE="/opt/{{APP_NAME}}/.gh_token"
log() { echo "$(date): $1" | tee -a /opt/{{APP_NAME}}/update.log; }
log "Checking for updates..."
echo "$(cat $TOKEN_FILE)" | docker login ghcr.io -u ${{ GITHUB_USERNAME }} --password-stdin > /dev/null 2>&1
docker pull "$IMAGE:latest" > /dev/null 2>&1
# Compare image IDs (not manifest digests — multi-arch bug: RepoDigests != pull output)
REMOTE_ID=$(docker inspect "$IMAGE:latest" --format="{{.Id}}" 2>/dev/null || echo "")
LOCAL_ID=$(docker inspect "$CONTAINER_NAME" --format="{{.Image}}" 2>/dev/null || echo "")
if [ -z "$LOCAL_ID" ]; then
log "Container not running, will start..."
fi
if [ "$REMOTE_ID" != "$LOCAL_ID" ]; then
log "New version detected ($REMOTE_ID), deploying..."
docker stop "$CONTAINER_NAME" 2>/dev/null || true
docker rm "$CONTAINER_NAME" 2>/dev/null || true
SESSION_SECRET=$(openssl rand -base64 32 2>/dev/null || head -c32 /dev/urandom | base64)
docker run -d \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-p 127.0.0.1:8080:8080 \
-e SESSION_SECRET="$SESSION_SECRET" \
-v /opt/{{APP_NAME}}/data:/app/data \
"$IMAGE:latest"
log "SUCCESS: Deployed new version (image: $REMOTE_ID)"
else
log "No changes detected, skipping restart"
fi
Dockerfile (multi-stage):
golang:{{GOVSN}}-alpine + templ generate + go build -ldflags="-X main.Version=${VERSION}"alpine:3.21 (no root, healthcheck via TCP port check)8080otherSame structure but use placeholders that the agent/user must fill:
{{IMAGE_NAME}} = full image path{{SERVER_HOST}} = deploy server SSH host{{APP_DIR}} = path on server{{DEPLOY_TOKEN_PATH}} = path to ghcr.io tokenGo projects use environment variables directly (no .env by default). For production, pass via docker run -e VAR=value. For local dev, optionally add github.com/joho/godotenv if user requests .env support.
To show version in the app UI, inject via ldflags at build time:
go build -ldflags="-X main.Version=${VERSION}" -o app ./cmd/web/
In Go code:
var Version = "dev" // set via ldflags in Dockerfile
func main() {
cfg := config.Load()
cfg.Version = Version
}
In config.go:
type Config struct {
// ... other fields
Version string
}
Pass version to templates via page data structs:
type HomePageData struct {
Sessions []SessionCardData
Version string // added to struct
}
project/
├── cmd/web/main.go # Entry point
├── config/
│ ├── config.go # Configuration
│ ├── config_dev.go # Dev config
│ └── config_prod.go # Prod config
├── router/router.go # Main router
├── nats/nats.go # NATS setup
├── features/ # Self-contained features
│ ├── common/
│ │ ├── layouts/base.templ
│ │ └── components/
│ ├── index/ # Home page
│ ├── todos/ # CRUD example (NATS KV)
│ ├── counter/ # Global vs user state
│ ├── monitor/ # System info
│ ├── sortable/ # Lit + SortableJS
│ ├── reverse/ # Streaming demo
│ ├── whiteboard/ # [OPTIONAL] Fabric.js
│ └── voice-training/ # [OPTIONAL] LiveKit + Gemini
├── web/
│ └── resources/
│ └── styles/
│ └── styles.css # DaisyUI + Tailwind
├── go.mod
└── Taskfile.yml
Each feature in features/<name>/ is 100% self-contained:
go:embed)# To add: copy the feature directory to the project
cp -r boilerplate-go/assets/scaffold/features/whiteboard myproject/features/
# To remove: delete the directory
rm -rf myproject/features/whiteboard
// features/whiteboard/static.go
package whiteboard
import (
"embed"
"io/fs"
"net/http"
)
//go:embed static/*
var staticEmbed embed.FS
func StaticFS() http.FileSystem {
fsys, _ := fs.Sub(staticEmbed, "static")
return http.FS(fsys)
}
DaisyUI is ready-made TailwindCSS components. Always recommended for Go web projects because:
When NOT to use DaisyUI:
| Need | Solution | |------|----------| | Simple broadcast (1→N) | NATS Core | | Messages with history | JetStream | | Work queues | JetStream Consumer | | Simple Key-Value | JetStream KV | | High performance, low latency | NATS Core |
| Criteria | SQLite | PocketBase | |----------|--------|------------| | Multiple instances | ❌ | ✅ | | Built-in auth | ❌ | ✅ | | Automatic REST API | ❌ | ✅ | | Realtime subscriptions | ❌ | ✅ | | Migrations | Manual | Automatic | | Simplicity | ✅ | ✅ | | Local data only | ✅ | ❌ |
Activate LiveKit + Gemini when:
Do NOT activate (use features/ai/ goai module instead):
⚠️ CRITICAL: This boilerplate uses Datastar v1.0.2. Read this section carefully!
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js"></script>
The boilerplate includes datastar.js at web/resources/static/datastar/datastar.js.
<script defer type="module" src="/static/datastar/datastar.js"></script>
import 'https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js'
Your HTML template must include the Datastar script:
<!DOCTYPE html>
<html lang="en">
<head>
<script defer type="module" src="/static/datastar/datastar.js"></script>
</head>
<body>
{ children... }
</body>
</html>
| Attribute | Example | Purpose |
|-----------|---------|---------|
| data-on:click | data-on:click="@post('/api/action')" | Click handler |
| data-init | data-init="$count = 1" | Optional: executes expression on element init |
| data-signals | data-signals="{count: 0}" | Reactive state |
| data-bind | data-bind="model" | Two-way binding |
| data-text | data-text="$count" | Text content |
| data-class | data-class="{'text-primary': $active}" | Conditional classes |
| data-show | data-show="$visible" | Conditional visibility |
⚠️ Overusing signals typically indicates trying to manage state on the frontend.
Favor fetching current state from the backend rather than pre-loading and assuming frontend state is current.
Good rule of thumb:
Why this matters:
Standard forms (small data):
<form data-on:submit={`@post('/api/action', {contentType: 'form'})`}>
<input name="name" data-bind="name">
<button type="submit">Submit</button>
</form>
Backend handler:
func handler(w http.ResponseWriter, r *http.Request) {
sse := datastar.NewSSE(w, r)
// For production with large SSE payloads, enable compression:
// sse := datastar.NewSSE(w, r, datastar.WithCompression())
r.ParseForm() // Required for form contentType
name := r.FormValue("name")
// ...
}
⚠️ Important: Always use
{contentType: 'form'}for form submissions to send form-encoded data, not JSON signals.
⚠️ Read references/datastar/patterns.md for complete patterns.
// Backend: signals and patches
type CounterSignals struct {
Global uint32 `json:"global"`
User uint32 `json:"user"`
}
templ Counter(signals CounterSignals) {
<div data-signals={ templ.JSONString(signals) }}>
<button data-on:click={ datastar.PostSSE("/counter/increment/global") }>
+ Global
</button>
<span data-text="$global"></span>
</div>
}
// Backend: CRUD with NATS KV
js.Set("todos", "key", []byte(jsonData))
// Frontend: reactive signals
<div data-signals={ templ.JSONString(TodoSignals{
Todos: []*Todo{},
Mode: "all",
}) }>
<ul>
for _, todo := range mvc.Todos {
@TodoRow(todo)
}
</ul>
</div>
💡 Skill complementar: Para planejamento estratégico completo (shaping, análise de riscos, planejamento técnico, gates de qualidade), use a skill
cali-product-workflowvia/skill:cali-product-workflow. Ela gerencia todo o workflow de produto — este boilerplate cobre a parte de implementação.
| Reference | When to Read | What It Contains | |-----------|--------------|------------------| | references/README.md | First | Index with reading path | | references/templ/rules.md | Before generating ANY HTML | Zero-tolerance rules, anti-patterns, CI enforcement for Templ | | references/datastar/patterns.md | When using Datastar | Signals, SSE, events, indicators | | references/datastar/pitfall.md | When Datastar behaves unexpectedly | Known pitfalls and fixes | | references/datastar/toast.md | When adding notifications | Backend-driven toasts (zero JS, animated) | | references/datastar/versus_javascript.md | When deciding JS vs Datastar | Decision matrix per browser feature | | references/daisyui/datastar-integration.md | When combining DaisyUI + Datastar | Integration rules and pitfalls (modal, show, signals) | | DaisyUI llms.txt | When needing any DaisyUI component | All components, class names, syntax — fetch via curl for current info | | references/nats/when-to-use-jetstream.md | When configuring real-time | NATS Core vs JetStream vs KV | | references/voice-ai/when-to-use.md | When adding voice AI | LiveKit + Gemini | | references/whiteboard/fabric_patterns.md | When using whiteboard | Fabric.js + synchronization | | references/database/README.md | When choosing a database | SQLite vs PocketBase decision guide | | references/database/database.go + user_crud_example.templ | When implementing DB layer | Concrete setup and CRUD example | | references/examples/ | When implementing a specific UI pattern | Active search, click-to-edit, counter, file upload, infinite scroll, lazy load, todo MVC, whiteboard | | cali-product-workflow/references/tech-planning/generation-principles.md | Sempre | Princípios de geração de código (KISS, DRY, LoB, SoC) |
When updating an existing Go project to use Datastar v1, follow this checklist:
Datastar Script Setup
web/resources/static/datastar/<script defer type="module" src="/static/datastar/datastar.js"></script>data-init is optional (not required) - only use when you need to execute an action on page/component loadStatic Files Configuration
//go:build dev for dev, //go:build !dev for prodhttp.FileServerFS(os.DirFS(StaticDirectoryPath))embed.FS + hashfs.FileServercurl http://localhost:8080/static/datastar/datastar.js should return 200Form Migration
name attribute to all inputs/textareasdata-bind for two-way syncdata-on:input="$var = el.value" with data-bind="varName"JSON Escaping
json.Marshal approachVisibility Patterns
class="hidden" + data-show with data-show onlySee Common Pitfalls section below.
❌ Don't: Manual escaping with strings.ReplaceAll
s = strings.ReplaceAll(s, "\n", "\\n")
s = strings.ReplaceAll(s, "'", "\\'")
✅ Do: Use json.Marshal
func escapeForJS(s string) string {
b, _ := json.Marshal(s)
return string(b)
}
Note: json.Marshal returns the string with quotes included, so use %s directly in templates, not '%s'.
❌ Don't: Inputs without name attribute
<input data-bind="model">
✅ Do: Always include name
<input name="model" data-bind="model">
❌ Don't: Only data-on:input
<textarea data-on:input="$prompt = el.value">
✅ Do: Use data-bind
<textarea name="prompt" data-bind="prompt">
❌ Don't: Mix class="hidden" with data-show
<div data-show="$tab === 'a'" class="hidden">
✅ Do: Use only data-show
<div data-show="$tab === 'a'">
❌ Don't: Forget to reset signal
// Handler only saves, doesn't reset
repo.Save(data)
✅ Do: Reset signal after operation
repo.Save(data)
sse.MarshalAndPatchSignals(map[string]bool{"isLoading": false})
Cause: Malformed data-signals JSON Fix:
Cause: Inputs missing name attribute
Fix: Add name="fieldName" to all form inputs
Cause: Script not loaded Fix:
Cause: Typically a data-bind or signal definition issue Fix:
data-bind attribute is present on form elementsCause: Wrong data-on:click syntax
Fix: Use @post('/api/action') format with quotes and leading slash
Cause: Missing {contentType: 'form'} in action
Fix: Use @post('/api/action', {contentType: 'form'}) and call r.ParseForm() in handler
data-indicator@view-transition { navigation: auto; } for smooth navigationThis project follows strict engineering principles. Violations block merge.
No .go, .templ, or .js file shall exceed 500 lines.
handlers/chat.go (god function) → chat_send.go + chat_load.go + chat_types.godb/repository.go (all repos) → session_repo.go + message_repo.go + settings_repo.go + feedback_repo.goNever write this pattern manually:
var buf strings.Builder
component.Render(context.Background(), &buf)
sse.PatchElements(buf.String(), datastar.WithSelectorID("target"), datastar.WithModeInner())
Instead, use the shared helper on NarrativeService:
ns.renderAndPatch(sse, component, datastar.WithSelectorID("target"), datastar.WithModeInner())
Extract shared helpers BEFORE adding new code — never repeat patterns.
No function shall exceed 100 lines.
Functions >100 lines MUST be refactored into smaller focused functions before adding new logic.
Common god functions: sendChatMessage, HandleChat, large handler switch statements.
Prefer data-show, data-class, data-on:* over JavaScript DOM manipulation.
Only write JavaScript for things ONLY JS can do:
data-on:scroll in Datastar Free)@clipboard(), Free: JS)For visual feedback (class toggling, show/hide): ALWAYS use Datastar signals. Never classList.add/remove.
Small JS helpers should be colocated in Templ files via <script> blocks, not separate .js files.
fmt.Sprintf for HTML is forbiddenAll HTML rendering MUST use Templ components. If existing fmt.Sprintf HTML is found, convert to Templ before adding new code.
fmt.Sprintf with HTML tags can be replaced with a Templ componentAfter any browser-facing change:
agent-browser skill — navigate pages, click buttons, verify no JS errors in console.dogfood skill — systematically explore the feature, test edge cases, find bugs.Use skill("agent-browser") and skill("dogfood") tools to load them.
This boilerplate is inspired by Northstar by Delaney Johnson.
development
PocketBase v0.39+ development - API rules, auth, collections, SDK, realtime, files, Go/JS extending, deployment, production tuning.
tools
Auto-initialize structured documentation for any project using lat.md (knowledge graph of markdown files with [[wiki links]], // @lat: code refs, and semantic search). Detects cali-product-workflow artifacts (spec-product.md, spec-tech.md, critiques) and uses them as seed material. Falls back to extracting business rules, architecture, and design decisions directly from the codebase. Use when a project lacks structured documentation or when lat.md/ is missing. After seeding, lat.md extension hooks keep documentation alive automatically.
testing
[Cali] Server security audit and hardening for private servers behind Tailscale. Use when: auditing server security, hardening SSH/firewall/Docker, checking for vulnerabilities, setting up fail2ban, reviewing port exposure, or responding to security alerts. Covers 6 layers: CloudFlare, UFW, Tailscale, SSH, Docker, Application. Triggers: "server security", "security audit", "harden server", "SSH hardening", "firewall rules", "UFW config", "fail2ban", "port security", "Docker security", "vulnerability check", "security review".
tools
Run supply chain security scans before installing packages or before releases. Triggers when: user installs a package (npm, pip, go get, brew), user asks to 'scan dependencies', 'check vulnerabilities', 'supply chain', 'security audit', 'run trivy', 'run socket', or before any release/deployment. Also triggers on mentions of: socket.dev, trivy, OSV-scanner, dotenvx, CVE, dependency audit. Covers all four tools with concrete commands.