go-graphql — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited go-graphql (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Both production-grade Go GraphQL libraries are schema-first: write SDL (.graphql), bind Go resolvers. Pick the library, write the schema deliberately, and treat DataLoaders + complexity limits as non-optional.
context. A package-level DataLoader is a cross-tenant data leak.ctx.Done() and defer close(ch). Otherwise a disconnected client leaks a goroutine forever.| Library | Approach | Type safety | Build step | Pick when |
|---|---|---|---|---|
github.com/99designs/gqlgen | Codegen | Compile-time | go generate | Large schemas, Federation, strict types |
github.com/graph-gophers/graphql-go | Reflection | Parse-time | None | Small/medium schemas, simple pipeline |
github.com/graphql-go/graphql | Code-first | Runtime | None | Avoid — verbose, no SDL |
Read references/gqlgen.md for the codegen workflow, gqlgen.yml, DataLoaders, and Federation. Read references/graph-gophers.md for the reflection model, type mapping, and tracing.type User {
id: ID! # opaque scalar; never expose Int
email: String! # server can always return this → non-null
bio: String # may be unset → nullable
posts(first: Int = 10, after: String): PostConnection!
}
type CreateUserPayload { # mutation envelope: business errors as data
user: User
errors: [UserError!]!
}
type PostConnection { # Relay cursor pagination
edges: [PostEdge!]!
pageInfo: PageInfo!
}Nullability rule. A field is ! only when the server can always return a value. A resolver error on a non-null field nulls the parent object — cascade failures. Nullable fields null only themselves.
Pagination. Cursor connections beat offset pagination on large or write-heavy datasets — cursors are stable under concurrent inserts.
Mutation envelopes. Wrap mutation results so business-level errors (validation, conflict) become first-class data instead of polluting the top-level errors array.
// Good — resolver translates and delegates.
func (r *mutationResolver) CreateUser(ctx context.Context, in CreateUserInput) (*CreateUserPayload, error) {
user, err := r.users.Create(ctx, in.Email, in.Name)
if err != nil {
return nil, presentError(err)
}
return &CreateUserPayload{User: toGQLUser(user)}, nil
}
// Bad — SQL inside the resolver.
func (r *queryResolver) User(ctx context.Context, id string) (*User, error) {
row := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", id)
// ...
}Use per-type resolver structs (userResolver, postResolver) instead of one monolithic resolver. It scales with the schema.
A naive User.posts resolver fires one SQL query per user — O(n) round-trips. DataLoaders coalesce per-field loads within a single tick into one batched query.
// Good — per-request DataLoader in middleware.
func DataLoaderMiddleware(db *sql.DB, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
loaders := &Loaders{
PostsByUser: newPostsByUserLoader(r.Context(), db),
}
ctx := context.WithValue(r.Context(), loadersKey{}, loaders)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Bad — package-level DataLoader caches across requests.
var globalLoader = newPostsByUserLoader(context.Background(), db)Package-level DataLoaders silently serve user A's data to user B's request as long as the cached key matches. This is the most dangerous bug in Go GraphQL services.
Authenticate in HTTP middleware (extract identity, stash in ctx); authorize per-field via schema directives (@hasRole(role: ADMIN)) in gqlgen, or resolver-level checks in graph-gophers. Authorization policy belongs in the schema, not scattered across resolvers. See references/gqlgen.md.
Never surface raw error values — they leak SQL fragments and internals. Install an ErrorPresenter (gqlgen) or implement ResolverError (graph-gophers) that returns sanitized messages. Attach a stable code in extensions (NOT_FOUND, FORBIDDEN) for client handling. Use graphql.AddError(ctx, err) for non-fatal field errors with partial data.
Every subscription goroutine must defer close(ch) and select on ctx.Done() in both the receive and send branches:
go func() {
defer close(ch)
for {
select {
case <-ctx.Done(): return
case msg := <-sub:
select { case ch <- msg: case <-ctx.Done(): return }
}
}
}()Without this, every disconnected client leaks a goroutine.
extension.FixedComplexityLimit(200) (gqlgen) or graphql.MaxDepth(10) + MaxParallelism(10) (graph-gophers)| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Package-level DataLoader | Cross-tenant data leakage, stale cache | Construct per-request in middleware |
| SQL in resolver | Resolver becomes data layer; no batching | Delegate to service; load via DataLoader |
| Non-null field that can fail | Cascade-nulls the parent | Make it nullable; or guarantee in resolver |
Editing models_gen.go | Wiped on next codegen | Use autobind / models.<T>.model in gqlgen.yml |
| Introspection in production | Exposes full schema surface | Gate by env |
| Subscription goroutine leak | Each disconnect leaks a goroutine | defer close(ch) + select ctx.Done() |
| No complexity cap | Single deep query = CPU/memory DoS | FixedComplexityLimit(N) or persisted queries |
| Raw internal error to client | Leaks DB messages, stack traces | ErrorPresenter returning sanitized message |
int field for Int! in graph-gophers | Library expects int32 | Use int32 (or float64 for Float) |
errors: [UserError!]!ErrorPresenter (gqlgen) or ResolverError (graph-gophers) sanitizes internalsdefer close(ch) and selects on ctx.Done()gqlgen.yml, DataLoaders, Federation~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.