- Add avatar upload functionality with public URL access. - Implement user login with email and password authentication. - Create endpoints for fetching and updating the authenticated user's profile. - Set up database migrations for user profiles, including email and role management. - Introduce personal data management linked to user profiles. - Add email verification process with welcome email templates.
837 lines
27 KiB
Markdown
837 lines
27 KiB
Markdown
## Go style guide
|
|
|
|
MUST write valid Go v1.22+ code, best practices.
|
|
|
|
## Generated folders — do not edit
|
|
|
|
`encore.gen/` + `.encore/` CLI-regenerated; never edit.
|
|
|
|
`encore.app` at repo root = app manifest — CUE-formatted **text** file, not binary, despite `.app` extension. Read like any source file.
|
|
|
|
## Encore local MCP server
|
|
|
|
Local Encore MCP wired via `.mcp.json` (registered **`encore-local`**, tool prefix `mcp__encore-local__`). Prefer for live app introspection over `Read`/grep; use docs tools when unsure of Encore API surface.
|
|
|
|
### Tools
|
|
|
|
20 tools, all prefixed `mcp__encore-local__`. MCP client gets full catalog (names, descriptions, input schemas) via `tools/list` on session init — no need enumerate here.
|
|
|
|
### Important tools (runtime-only — no filesystem equivalent)
|
|
|
|
Do what `Glob`/`Grep`/`Read` can't. For static config (services, endpoints, topics, schemas), see "When NOT to use it" below.
|
|
|
|
- **`call_endpoint`** — args: `service`, `endpoint`, `method`, `path`, `payload` (JSON string with body/query/headers/path-params), optional `auth_token` / `auth_payload` / `correlation_id`. Optional `retry_until: { predicate, timeout_ms?, interval_ms?, fail_on_timeout? }` polls **server-side** until predicate matches — predicate = `status: 200` OR `body_path: { path: ".events.0.orderID", equals: 7 }` OR `body_jq: ".events | length > 0"` (minimal subset: `<path> | length <op> N` or `<path>` truthy). Use instead of agent-side polling loops for eventually-consistent reads. **auto-starts app** if not running. Use instead of `curl` to test endpoints.
|
|
- **`wait_for_subscription_message`** — args: `topic`, optional `subscription`, `timeout_ms` (default 10000), `since` (ISO/RFC3339), `match` (top-level key/value JSON filter, e.g. `{"CustomerID":"cust_42"}`). Blocks until the next message on the topic is fully processed by a subscription handler (returns or errors), then returns `{outcome, payload, duration_ms, handler_error, trace_id}`. Use to bridge async Pub/Sub work into a synchronous verify step — beats polling read-side endpoints + introspecting via `get_pubsub`/`get_traces`.
|
|
- **`query_database`** — args: `queries` (array of `{database, query}`). Runs SQL against named DBs, multiple queries one call. Beats `encore db conn-uri` + `psql` round-trips.
|
|
- **`get_traces`** — args: optional `service`, `endpoint`, `error` (`"true"`/`"false"`), `start_time` / `end_time` (ISO), `limit`. Returns recent request traces (timing, status, ids).
|
|
- **`get_trace_spans`** — args: `trace_ids` (array IDs from `get_traces`). Returns full per-span details for deep debugging.
|
|
- **`get_objects`** — args: `buckets` (array of bucket names). Lists objects + metadata in storage buckets.
|
|
- **`search_docs`** — args: `query`, optional `hits_per_page`, `page`, `facet_filters`. Algolia-backed Encore docs search.
|
|
- **`get_docs`** — args: `paths` (array of doc paths, e.g. `/docs/go/primitives/databases`). Fetches full doc pages found via `search_docs`.
|
|
|
|
### When to reach for it
|
|
|
|
| Situation | Tool |
|
|
|---|---|
|
|
| "What services / endpoints / databases exist?" | `get_metadata` once |
|
|
| "Test my new POST /orders endpoint" | `call_endpoint` (auto-starts app) |
|
|
| "Did the Pub/Sub handler run after I published?" | `wait_for_subscription_message` |
|
|
| "Endpoint output is eventually consistent (e.g. read-after-publish)" | `call_endpoint` with `retry_until` |
|
|
| "Why did last request fail?" | `get_traces` then `get_trace_spans` |
|
|
| "How does Encore API surface work?" | `search_docs` then `get_docs` |
|
|
|
|
- **MCP only for runtime data** (`call_endpoint`, `query_database`, `get_traces`, `get_objects`, `search_docs`/`get_docs`). For **static structure** declared in source (services, endpoints, topics, subscriptions, schemas, secrets, middleware, cron jobs), `Glob`+`Grep`+`Read` faster + more reliable than `get_*` tool.
|
|
|
|
### Cloud MCP (deployed environments)
|
|
|
|
If deployed Encore Cloud, also register `encore-cloud` via `claude mcp add --transport http encore-cloud https://api.encore.cloud/mcp` for prod traces / deploy state — `encore-local` only sees local app.
|
|
|
|
## Encore check (verify app builds + endpoints work)
|
|
|
|
`encore check` compiles, boots, health-checks app, optionally runs `curl` once healthy. Use instead of manual `encore run + healthz poll + curl`.
|
|
|
|
```bash
|
|
encore check # compile + boot only
|
|
encore check 'curl /ping' # GET a relative path
|
|
encore check 'curl /orders -X POST -d "{\"customer_id\":\"c1\",\"amount_cents\":1999}"' # POST with JSON body (flags after path)
|
|
encore check 'curl /a; curl /b' # chain in one quoted string
|
|
encore check 'curl /a' 'curl /b' # or as separate quoted args
|
|
encore check < commands.txt # one curl per line, piped from file
|
|
```
|
|
|
|
Rules embedded curl DSL:
|
|
- **Paths relative** (start with `/`). Use `curl /orders/1`, **not** `curl http://localhost:4000/orders/1` — host/port supplied by `encore check`. Absolute URL fails with `curl path "http..." must be relative`.
|
|
- **Path first, flags after.** `curl /orders -X POST -d '...'` works; `curl -X POST /orders ...` fails — parser reads first token after `curl` as path, rejects flags there (`curl path "-X" must be relative`).
|
|
- One curl per quoted command. Chain `;` inside one quoted string, pass multiple quoted args, or pipe file one curl per line.
|
|
|
|
## Pub/sub verification
|
|
|
|
- Delivery async + at-least-once + not order-preserving.
|
|
- For "most-recent first" by publish order, DO NOT sort by `created_at = NOW()` or subscription-side row id — both subscription-insert order, at-least-once can flip. Sort by column monotonic with publish (entity id from event payload, or publish-time sequence in event).
|
|
- Handler-only checks, skip infra: `et.Topic(T).PublishedMessages()`.
|
|
|
|
## Encore Go domain knowledge
|
|
|
|
### Application structure
|
|
|
|
Encore uses monorepo design — one app = entire backend. Enables distributed tracing + Encore Flow via unified application model. Supports monolith + microservices with monolith-style DX.
|
|
|
|
Directory structure:
|
|
/app-name
|
|
encore.app
|
|
service1/
|
|
migrations/
|
|
1_create_table.up.sql
|
|
service1.go
|
|
service1_test.go
|
|
service2/
|
|
service2.go
|
|
|
|
Sub-packages internal to services, cannot define APIs, used for helpers + code organization.
|
|
|
|
Large apps — group related services into system directories (logical groupings, no special runtime behavior):
|
|
/app-name
|
|
encore.app
|
|
system1/
|
|
service1/
|
|
service2/
|
|
system2/
|
|
service3/
|
|
|
|
### API definition
|
|
|
|
Create type-safe APIs from regular Go functions via //encore:api annotation.
|
|
|
|
Access controls:
|
|
- public: Accessible to anyone on internet
|
|
- private: Only accessible within app + via cron jobs
|
|
- auth: Public but requires valid auth
|
|
|
|
Function signatures:
|
|
func Foo(ctx context.Context, p *Params) (*Response, error) // full
|
|
func Foo(ctx context.Context) (*Response, error) // response only
|
|
func Foo(ctx context.Context, p *Params) error // request only
|
|
func Foo(ctx context.Context) error // minimal
|
|
|
|
Request/response data locations:
|
|
- header: Use `header` tag for HTTP headers
|
|
- query: Default GET/HEAD/DELETE, uses snake_case, supports basic types/slices
|
|
- body: Default other methods, uses `json` tag, supports complex types
|
|
|
|
Path parameters: Use :name for variables, *name for wildcards. Place at end of path.
|
|
|
|
Sensitive data:
|
|
- Field level: `encore:"sensitive"` tag, auto-redacted in tracing
|
|
- Endpoint level: Add `sensitive` to //encore:api annotation
|
|
|
|
Type support by location:
|
|
- headers/path: bool, numeric, string, time.Time, UUID, json.RawMessage
|
|
- query: All above plus lists
|
|
- body: All types including structs, maps, pointers
|
|
|
|
### Services
|
|
|
|
Service defined by creating at least one API within Go package. Package name = service name.
|
|
|
|
//encore:service annotation enables custom init + graceful shutdown:
|
|
|
|
type Service struct {
|
|
// Dependencies here
|
|
}
|
|
|
|
func initService() (*Service, error) {
|
|
// Initialization code
|
|
}
|
|
|
|
//encore:api public
|
|
func (s *Service) MyAPI(ctx context.Context) error {
|
|
// API implementation
|
|
}
|
|
|
|
Graceful shutdown via Shutdown method:
|
|
func (s *Service) Shutdown(force context.Context)
|
|
- Graceful phase: Several seconds for completion
|
|
- Forced phase: When force context canceled, terminate immediately
|
|
|
|
### Raw endpoints
|
|
|
|
Lower-level HTTP access (webhooks, WebSockets):
|
|
|
|
//encore:api public raw
|
|
func Webhook(w http.ResponseWriter, req *http.Request) {
|
|
// Process raw HTTP request
|
|
}
|
|
|
|
//encore:api public raw method=POST path=/webhook/:id
|
|
func Webhook(w http.ResponseWriter, req *http.Request) {
|
|
id := encore.CurrentRequest().PathParams.Get("id")
|
|
}
|
|
|
|
### SQL databases
|
|
|
|
Encore treats SQL databases as logical resources with native PostgreSQL support.
|
|
|
|
Create database:
|
|
var tododb = sqldb.NewDatabase("todo", sqldb.DatabaseConfig{
|
|
Migrations: "./migrations",
|
|
})
|
|
|
|
Migration naming: number_description.up.sql (e.g., 1_create_table.up.sql)
|
|
Migrations folder structure:
|
|
service/
|
|
migrations/
|
|
1_create_table.up.sql
|
|
2_add_field.up.sql
|
|
service.go
|
|
|
|
Data operations:
|
|
// Insert
|
|
_, err := tododb.Exec(ctx, `
|
|
INSERT INTO todo_item (id, title, done)
|
|
VALUES ($1, $2, $3)
|
|
`, id, title, done)
|
|
|
|
// Query
|
|
err := tododb.QueryRow(ctx, `
|
|
SELECT id, title, done FROM todo_item LIMIT 1
|
|
`).Scan(&item.ID, &item.Title, &item.Done)
|
|
// Use errors.Is(err, sqldb.ErrNoRows) for no results
|
|
|
|
Always Scan into typed struct fields, never `interface{}` slots — compiler enforces column-to-field shape + catches drift between SELECT list + destination struct.
|
|
|
|
CLI commands:
|
|
- encore db shell database-name [--env=name] - Open psql shell
|
|
- encore db conn-uri database-name [--env=name] - Output connection string
|
|
- encore db proxy [--env=name] - Setup local connection proxy
|
|
|
|
### External databases
|
|
|
|
Existing databases — create dedicated package with lazy connection pool:
|
|
|
|
package externaldb
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/jackc/pgx/v4/pgxpool"
|
|
"go4.org/syncutil"
|
|
)
|
|
|
|
func Get(ctx context.Context) (*pgxpool.Pool, error) {
|
|
err := once.Do(func() error {
|
|
var err error
|
|
pool, err = setup(ctx)
|
|
return err
|
|
})
|
|
return pool, err
|
|
}
|
|
|
|
var (
|
|
once syncutil.Once
|
|
pool *pgxpool.Pool
|
|
)
|
|
|
|
var secrets struct {
|
|
ExternalDBPassword string
|
|
}
|
|
|
|
func setup(ctx context.Context) (*pgxpool.Pool, error) {
|
|
connString := fmt.Sprintf("postgresql://%s:%s@hostname:port/dbname?sslmode=require",
|
|
"user", secrets.ExternalDBPassword)
|
|
return pgxpool.Connect(ctx, connString)
|
|
}
|
|
|
|
Works with Cassandra, DynamoDB, BigTable, MongoDB, Neo4j, other services.
|
|
|
|
### Shared databases
|
|
|
|
Default: per-service databases for isolation. Share via sqldb.Named:
|
|
|
|
// In report service, access todo service's database:
|
|
var todoDB = sqldb.Named("todo")
|
|
|
|
//encore:api method=GET path=/report/todo
|
|
func CountCompletedTodos(ctx context.Context) (*ReportResponse, error) {
|
|
var report ReportResponse
|
|
err := todoDB.QueryRow(ctx,`
|
|
SELECT COUNT(*) FROM todo_item WHERE completed = TRUE
|
|
`).Scan(&report.Total)
|
|
return &report, err
|
|
}
|
|
|
|
### Cron jobs
|
|
|
|
Declarative periodic tasks. Does not run locally or in Preview Environments.
|
|
|
|
import "encore.dev/cron"
|
|
|
|
var _ = cron.NewJob("welcome-email", cron.JobConfig{
|
|
Title: "Send welcome emails",
|
|
Every: 2 * cron.Hour,
|
|
Endpoint: SendWelcomeEmail,
|
|
})
|
|
|
|
//encore:api private
|
|
func SendWelcomeEmail(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
Scheduling options:
|
|
- Every: Must divide 24 hours evenly (e.g., 10 * cron.Minute, 6 * cron.Hour)
|
|
- Schedule: Cron expressions (e.g., "0 4 15 * *" for 4am UTC on 15th)
|
|
|
|
Requirements: Endpoints must be idempotent, no request parameters, signature func(context.Context) error or func(context.Context) (*T, error)
|
|
|
|
### Caching
|
|
|
|
Redis-based distributed caching.
|
|
|
|
import "encore.dev/storage/cache"
|
|
|
|
var MyCacheCluster = cache.NewCluster("my-cache-cluster", cache.ClusterConfig{
|
|
EvictionPolicy: cache.AllKeysLRU,
|
|
})
|
|
|
|
// Keyspace with type safety
|
|
var RequestsPerUser = cache.NewIntKeyspace[auth.UID](cluster, cache.KeyspaceConfig{
|
|
KeyPattern: "requests/:key",
|
|
DefaultExpiry: cache.ExpireIn(10 * time.Second),
|
|
})
|
|
|
|
// Structured keys
|
|
type MyKey struct {
|
|
UserID auth.UID
|
|
ResourcePath string
|
|
}
|
|
var ResourceRequestsPerUser = cache.NewIntKeyspace[MyKey](cluster, cache.KeyspaceConfig{
|
|
KeyPattern: "requests/:UserID/:ResourcePath",
|
|
DefaultExpiry: cache.ExpireIn(10 * time.Second),
|
|
})
|
|
|
|
Supports strings, integers, floats, structs, sets, ordered lists.
|
|
|
|
### Object storage
|
|
|
|
Cloud-agnostic API compatible with S3, GCS, S3-compatible services.
|
|
|
|
var ProfilePictures = objects.NewBucket("profile-pictures", objects.BucketConfig{
|
|
Versioned: false,
|
|
})
|
|
|
|
// Public bucket with CDN
|
|
var PublicAssets = objects.NewBucket("public-assets", objects.BucketConfig{
|
|
Public: true,
|
|
})
|
|
|
|
Operations: Upload, Download, List, Remove, Attrs, Exists
|
|
|
|
Bucket references for permissions:
|
|
type myPerms interface {
|
|
objects.Downloader
|
|
objects.Uploader
|
|
}
|
|
ref := objects.BucketRef[myPerms](bucket)
|
|
|
|
### Pub/Sub
|
|
|
|
Async event broadcasting with automatic infra provisioning.
|
|
|
|
type SignupEvent struct{ UserID int }
|
|
|
|
var Signups = pubsub.NewTopic[*SignupEvent]("signups", pubsub.TopicConfig{
|
|
DeliveryGuarantee: pubsub.AtLeastOnce,
|
|
})
|
|
|
|
// Publishing
|
|
messageID, err := Signups.Publish(ctx, &SignupEvent{UserID: id})
|
|
|
|
// Topic reference
|
|
signupRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](Signups)
|
|
|
|
// Subscribing
|
|
var _ = pubsub.NewSubscription(
|
|
user.Signups, "send-welcome-email",
|
|
pubsub.SubscriptionConfig[*SignupEvent]{
|
|
Handler: SendWelcomeEmail,
|
|
},
|
|
)
|
|
|
|
// Method handler with dependency injection
|
|
var _ = pubsub.NewSubscription(
|
|
user.Signups, "send-welcome-email",
|
|
pubsub.SubscriptionConfig[*SignupEvent]{
|
|
Handler: pubsub.MethodHandler((*Service).SendWelcomeEmail),
|
|
},
|
|
)
|
|
|
|
Delivery guarantees:
|
|
- AtLeastOnce: Handlers must be idempotent
|
|
- ExactlyOnce: Stronger guarantees (AWS: 300 msg/sec, GCP: 3000+ msg/sec)
|
|
|
|
Ordering: Use OrderingAttribute matching pubsub-attr tag
|
|
|
|
Testing:
|
|
msgs := et.Topic(Signups).PublishedMessages()
|
|
assert.Len(t, msgs, 1)
|
|
|
|
### Secrets
|
|
|
|
Built-in secrets manager for API keys, passwords, private keys.
|
|
|
|
var secrets struct {
|
|
SSHPrivateKey string
|
|
GitHubAPIToken string
|
|
}
|
|
|
|
func callGitHub(ctx context.Context) {
|
|
req.Header.Add("Authorization", "token " + secrets.GitHubAPIToken)
|
|
}
|
|
|
|
CLI management:
|
|
- encore secret set --type production secret-name
|
|
- encore secret set --type development secret-name
|
|
- encore secret set --env env-name secret-name (env-specific override)
|
|
|
|
Types: production (prod), development (dev), preview (pr), local
|
|
|
|
Local override via .secrets.local.cue:
|
|
GitHubAPIToken: "my-local-override-token"
|
|
|
|
### API calls
|
|
|
|
Call APIs like regular functions with automatic type checking:
|
|
|
|
import "encore.app/hello"
|
|
|
|
//encore:api public
|
|
func MyOtherAPI(ctx context.Context) error {
|
|
resp, err := hello.Ping(ctx, &hello.PingParams{Name: "World"})
|
|
if err == nil {
|
|
log.Println(resp.Message) // "Hello, World!"
|
|
}
|
|
return err
|
|
}
|
|
|
|
### Errors
|
|
|
|
Structured errors via encore.dev/beta/errs package.
|
|
|
|
return &errs.Error{
|
|
Code: errs.NotFound,
|
|
Message: "sprocket not found",
|
|
}
|
|
// Returns HTTP 404 {"code": "not_found", "message": "sprocket not found"}
|
|
|
|
Wrapping:
|
|
errs.Wrap(err, msg, metaPairs...)
|
|
errs.WrapCode(err, code, msg, metaPairs...)
|
|
|
|
Builder pattern:
|
|
eb := errs.B().Meta("board_id", params.ID)
|
|
return eb.Code(errs.NotFound).Msg("board not found").Err()
|
|
|
|
Error codes: OK(200), Canceled(499), Unknown(500), InvalidArgument(400), DeadlineExceeded(504), NotFound(404), AlreadyExists(409), PermissionDenied(403), ResourceExhausted(429), FailedPrecondition(400), Aborted(409), OutOfRange(400), Unimplemented(501), Internal(500), Unavailable(503), DataLoss(500), Unauthenticated(401)
|
|
|
|
Inspection: errs.Code(err), errs.Meta(err), errs.Details(err)
|
|
|
|
### Authentication
|
|
|
|
Flexible auth with different access levels.
|
|
|
|
import "encore.dev/beta/auth"
|
|
|
|
// Basic
|
|
//encore:authhandler
|
|
func AuthHandler(ctx context.Context, token string) (auth.UID, error) {
|
|
// Validate token and return user ID
|
|
}
|
|
|
|
// With user data
|
|
type Data struct {
|
|
Username string
|
|
}
|
|
|
|
//encore:authhandler
|
|
func AuthHandler(ctx context.Context, token string) (auth.UID, *Data, error) {
|
|
// Return user ID and custom data
|
|
}
|
|
|
|
// Structured auth params
|
|
type MyAuthParams struct {
|
|
SessionCookie *http.Cookie `cookie:"session"`
|
|
ClientID string `query:"client_id"`
|
|
Authorization string `header:"Authorization"`
|
|
}
|
|
|
|
//encore:authhandler
|
|
func AuthHandler(ctx context.Context, p *MyAuthParams) (auth.UID, error) {
|
|
// Process structured auth params
|
|
}
|
|
|
|
Usage: auth.Data(), auth.UserID()
|
|
Override for testing: auth.WithContext(ctx, auth.UID("my-user-id"), &MyAuthData{})
|
|
|
|
Error handling:
|
|
return "", &errs.Error{
|
|
Code: errs.Unauthenticated,
|
|
Message: "invalid token",
|
|
}
|
|
|
|
### Configuration
|
|
|
|
Env-specific config via CUE files.
|
|
|
|
package mysvc
|
|
|
|
import "encore.dev/config"
|
|
|
|
type SomeConfigType struct {
|
|
ReadOnly config.Bool
|
|
Example config.String
|
|
}
|
|
|
|
var cfg *SomeConfigType = config.Load[*SomeConfigType]()
|
|
|
|
CUE tags for constraints:
|
|
type FooBar {
|
|
A int `cue:">100"`
|
|
B int `cue:"A-50"`
|
|
C int `cue:"A+B"`
|
|
}
|
|
|
|
Config types: config.String, config.Bool, config.Int, config.Float64, config.Time, config.UUID, config.Value[T], config.Values[T]
|
|
|
|
Meta values:
|
|
- APIBaseURL, Environment.Name, Environment.Type (production/development/ephemeral/test), Environment.Cloud (aws/gcp/encore/local)
|
|
|
|
Testing: et.SetCfg(cfg.SendEmails, true)
|
|
|
|
CUE patterns:
|
|
- Defaults: value: type | *default_value
|
|
- Switch: array with conditionals, take [0]
|
|
|
|
### CORS
|
|
|
|
Configure in encore.app file:
|
|
- debug: Enable CORS debug logging
|
|
- allow_headers: Additional accepted headers ("*" allows all)
|
|
- expose_headers: Additional exposed headers
|
|
- allow_origins_without_credentials: Defaults to ["*"]
|
|
- allow_origins_with_credentials: For authenticated requests, supports wildcards like "https://*.example.com"
|
|
|
|
### Metadata
|
|
|
|
Access app + request info via encore.dev package.
|
|
|
|
// Application metadata
|
|
meta := encore.Meta()
|
|
// meta.AppID, meta.APIBaseURL, meta.Environment, meta.Build, meta.Deploy
|
|
|
|
// Request metadata
|
|
req := encore.CurrentRequest()
|
|
// req.Service, req.Endpoint, req.Path, req.StartTime
|
|
|
|
// Cloud-specific behavior
|
|
switch encore.Meta().Environment.Cloud {
|
|
case encore.CloudAWS:
|
|
return writeIntoRedshift(ctx, action, user)
|
|
case encore.CloudGCP:
|
|
return writeIntoBigQuery(ctx, action, user)
|
|
}
|
|
|
|
### Middleware
|
|
|
|
Reusable code running before/after API requests.
|
|
|
|
//encore:middleware global target=all
|
|
func ValidationMiddleware(req middleware.Request, next middleware.Next) middleware.Response {
|
|
payload := req.Data().Payload
|
|
if validator, ok := payload.(interface { Validate() error }); ok {
|
|
if err := validator.Validate(); err != nil {
|
|
err = errs.WrapCode(err, errs.InvalidArgument, "validation failed")
|
|
return middleware.Response{Err: err}
|
|
}
|
|
}
|
|
return next(req)
|
|
}
|
|
|
|
// With dependency injection
|
|
//encore:middleware target=all
|
|
func (s *Service) MyMiddleware(req middleware.Request, next middleware.Next) middleware.Response {
|
|
// Implementation
|
|
}
|
|
|
|
// Tag-based targeting
|
|
//encore:middleware target=tag:cache
|
|
func CachingMiddleware(req middleware.Request, next middleware.Next) middleware.Response {
|
|
// ...
|
|
}
|
|
|
|
//encore:api public method=GET path=/user/:id tag:cache
|
|
func GetUser(ctx context.Context, id string) (*User, error) {
|
|
// Implementation
|
|
}
|
|
|
|
Ordering: Global before service-specific, lexicographic by filename.
|
|
|
|
### Mocking
|
|
|
|
Built-in mocking for isolated testing.
|
|
|
|
// Mock endpoint for single test
|
|
func Test_Something(t *testing.T) {
|
|
t.Parallel()
|
|
et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
|
|
return &products.PriceResponse{Price: 100}, nil
|
|
})
|
|
}
|
|
|
|
// Mock endpoint for all tests in package
|
|
func TestMain(m *testing.M) {
|
|
et.MockEndpoint(products.GetPrice, func(ctx context.Context, p *products.PriceParams) (*products.PriceResponse, error) {
|
|
return &products.PriceResponse{Price: 100}, nil
|
|
})
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// Mock entire service
|
|
et.MockService("products", &products.Service{
|
|
SomeField: "a testing value",
|
|
})
|
|
|
|
// Type-safe service mocking
|
|
et.MockService[products.Interface]("products", &myMockObject{})
|
|
|
|
### Testing
|
|
|
|
Run tests: encore test ./...
|
|
Supports all standard go test flags. Built-in tracing at localhost:9400.
|
|
|
|
Database testing:
|
|
- Automatic setup in separate cluster, optimized for speed
|
|
- Temporary databases: et.NewTestDatabase() creates isolated, fully migrated DB
|
|
|
|
Service structs: Lazy init, instance sharing between tests
|
|
- Isolate: et.EnableServiceInstanceIsolation()
|
|
|
|
### Validation
|
|
|
|
Automatic request validation via Validate() method.
|
|
|
|
type MyRequest struct {
|
|
Email string
|
|
}
|
|
|
|
func (r *MyRequest) Validate() error {
|
|
if !isValidEmail(r.Email) {
|
|
return &errs.Error{Code: errs.InvalidArgument, Message: "invalid email"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
Validation runs after deserialization, before handler. Non-errs.Error errors become InvalidArgument (HTTP 400).
|
|
|
|
### CGO
|
|
|
|
Enable in encore.app:
|
|
{
|
|
"id": "my-app-id",
|
|
"build": {
|
|
"cgo_enabled": true
|
|
}
|
|
}
|
|
Uses Ubuntu builder with gcc. Libraries must support static linking.
|
|
|
|
### Clerk auth
|
|
|
|
Implement Clerk auth:
|
|
|
|
package auth
|
|
|
|
import "github.com/clerkinc/clerk-sdk-go/clerk"
|
|
|
|
type Service struct {
|
|
client clerk.Client
|
|
}
|
|
|
|
func initService() (*Service, error) {
|
|
client, err := clerk.NewClient(secrets.ClientSecretKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Service{client: client}, nil
|
|
}
|
|
|
|
type UserData struct {
|
|
ID string
|
|
Username *string
|
|
FirstName *string
|
|
LastName *string
|
|
ProfileImageURL string
|
|
PrimaryEmailAddressID *string
|
|
EmailAddresses []clerk.EmailAddress
|
|
}
|
|
|
|
//encore:authhandler
|
|
func (s *Service) AuthHandler(ctx context.Context, token string) (auth.UID, *UserData, error) {
|
|
// Token verification and user data retrieval
|
|
}
|
|
|
|
Set secrets:
|
|
- encore secret set --prod ClientSecretKey
|
|
- encore secret set --dev ClientSecretKey
|
|
|
|
### Dependency injection
|
|
|
|
Add dependencies as struct fields for easy testing:
|
|
|
|
package email
|
|
|
|
//encore:service
|
|
type Service struct {
|
|
sendgridClient *sendgrid.Client
|
|
}
|
|
|
|
func initService() (*Service, error) {
|
|
client, err := sendgrid.NewClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &Service{sendgridClient: client}, nil
|
|
}
|
|
|
|
//encore:api private
|
|
func (s *Service) Send(ctx context.Context, p *SendParams) error {
|
|
// Use s.sendgridClient
|
|
}
|
|
|
|
// For testing, use interface
|
|
type sendgridClient interface {
|
|
SendEmail(...)
|
|
}
|
|
|
|
func TestFoo(t *testing.T) {
|
|
svc := &Service{sendgridClient: &myMockClient{}}
|
|
// Test
|
|
}
|
|
|
|
### Pub/Sub outbox
|
|
|
|
Transactional outbox pattern for database + Pub/Sub consistency.
|
|
|
|
var SignupsTopic = pubsub.NewTopic[*SignupEvent](/* ... */)
|
|
ref := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic)
|
|
ref = outbox.Bind(ref, outbox.TxPersister(tx))
|
|
|
|
Required schema:
|
|
CREATE TABLE outbox (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
topic TEXT NOT NULL,
|
|
data JSONB NOT NULL,
|
|
inserted_at TIMESTAMPTZ NOT NULL
|
|
);
|
|
CREATE INDEX outbox_topic_idx ON outbox (topic, id);
|
|
|
|
Relay setup:
|
|
type Service struct {
|
|
signupsRef pubsub.Publisher[*SignupEvent]
|
|
}
|
|
|
|
func initService() (*Service, error) {
|
|
relay := outbox.NewRelay(outbox.SQLDBStore(db))
|
|
signupsRef := pubsub.TopicRef[pubsub.Publisher[*SignupEvent]](SignupsTopic)
|
|
outbox.RegisterTopic(relay, signupsRef)
|
|
go relay.PollForMessage(context.Background(), -1)
|
|
return &Service{signupsRef: signupsRef}, nil
|
|
}
|
|
|
|
Supports: encore.dev/storage/sqldb, database/sql, github.com/jackc/pgx/v5
|
|
|
|
### Encore Toolbar (frontend dev panel)
|
|
|
|
Dev-only browser panel: intercepts frontend HTTP calls, captures Encore trace IDs, jumps to traces in the dashboard. Install: add `<script src="https://encore.dev/encore-toolbar.js"></script>` early in `<head>` (no `async`/`defer`). Reach for it when a frontend talks to an Encore Go backend.
|
|
|
|
### Example apps
|
|
|
|
- Hello World: https://github.com/encoredev/examples/tree/main/hello-world
|
|
- URL Shortener: https://github.com/encoredev/examples/tree/main/url-shortener
|
|
- Uptime Monitor: https://github.com/encoredev/examples/tree/main/uptime
|
|
|
|
## Encore CLI reference
|
|
|
|
Execution:
|
|
- encore help [command] - Lists available commands (or shows usage for specific command). Reach for it when unsure which subcommand or flag to use.
|
|
- encore run [--debug] [--watch=true] [-p port] [flags] - Runs app. Before starting yourself, check if already running by polling `http://localhost:<port>/__encore/healthz` (default port `4000`) — `200` = healthy, hit endpoints directly without re-running.
|
|
- encore check ['curl ...'; 'curl ...'] | encore check < commands.txt - Compile + boot app, verify every service healthy, optionally run curl commands. See dedicated section above.
|
|
- encore test ./... [go test flags] - Wraps `go test ./...` with Encore's infra provisioning — boots isolated test databases, Pub/Sub, etc. before handing off to `go test`. Accepts all `go test` flags, plus `--prepare`, `--codegen-debug`, `--trace`.
|
|
- encore exec path/to/script [args...] - Run an executable script against the local Encore app
|
|
|
|
App management:
|
|
- encore app clone [app-id] [directory] - Clone app
|
|
- encore app create [name] [--example=name] [-l go] - Create new app
|
|
- encore app init [name] - Create from existing repo
|
|
- encore app link [app-id] [-f] - Link app with server
|
|
|
|
Authentication:
|
|
- encore auth login/logout/signup/whoami
|
|
|
|
Daemon:
|
|
- encore daemon - Restart daemon
|
|
- encore daemon env - Output environment info
|
|
|
|
Database:
|
|
- encore db shell database-name [--env=name] - psql shell (--write, --admin, --superuser)
|
|
- encore db conn-uri database-name [--env=name] - Connection string
|
|
- encore db proxy [--env=name] - Local proxy
|
|
- encore db reset <db-names...|--all> - Reset databases
|
|
|
|
Code generation:
|
|
- encore gen client [app-id] [--env=name] [--lang=lang] - Generate API client
|
|
Languages: go, typescript, javascript, openapi
|
|
|
|
Logging:
|
|
- encore logs [--env=prod] [--json] [-q] - Stream logs
|
|
|
|
Kubernetes:
|
|
- encore k8s configure --env=ENV_NAME - Update kubectl config
|
|
|
|
Secrets:
|
|
- encore secret set --type TYPE <secret-name> (types: production, development, preview, local)
|
|
- encore secret set --env env-name <secret-name>
|
|
- encore secret list [keys...]
|
|
- encore secret delete <id>
|
|
|
|
Namespaces (infrastructure environments, alias `encore ns`):
|
|
- encore namespace list [--output=columns|json] - List infra namespaces
|
|
- encore namespace create NAME - Create a namespace
|
|
- encore namespace switch [--create] NAME - Switch active namespace
|
|
- encore namespace delete NAME - Delete a namespace
|
|
|
|
MCP (programmatic access to the local app — same surface as `.mcp.json`):
|
|
- encore mcp run - stdio-based MCP session
|
|
- encore mcp start - SSE-based MCP session; prints the SSE URL
|
|
|
|
Config:
|
|
- encore config <key> [<value>] [--app|--global|--all] - Get/set CLI configuration
|
|
|
|
Telemetry:
|
|
- encore telemetry - Status
|
|
- encore telemetry enable / disable - Toggle telemetry reporting
|
|
|
|
LLM rules:
|
|
- encore llm-rules init - Generate LLM rule files for this project
|
|
|
|
Random data (utility):
|
|
- encore rand uuid [-1|-4|-6|-7] - Generate UUID (default v4)
|
|
- encore rand bytes N [-f format] - Generate N random bytes
|
|
- encore rand words [--sep=SEP] NUM - Generate memorable passphrase
|
|
|
|
Deploy (alpha):
|
|
- encore alpha deploy --env=<name> (--commit=<sha> | --branch=<name>) - Deploy app to a cloud env
|
|
|
|
Version:
|
|
- encore version - Report version
|
|
- encore version update - Check and apply updates
|
|
|
|
Build:
|
|
- encore build docker IMAGE_TAG [--base string] [--push] [--cgo] [--os] [--arch] - Build Docker image |