ballerina

Use this whenever you are working with ballerina code or editing .bal files — project layout, the bal library lookup flow, verifying the gateway's signed assertion on a contract-backed service, and th

SKILL.md

GitHub

Ballerina

Load code-rules.md before writing any .bal code. This contains best practices, syntaxes and common patterns for Ballerina code.

Development workflow

Creating a New Project

When the user asks to create a new project, service, or program from scratch:

bal new <project-name>   # scaffolds main.bal + Ballerina.toml
cd <project-name>

Write tests only when the user asks — then load tests.md.

Working with OpenAPI Specifications

Load openapi.md before writing a service or a client that has an OpenAPI document — one this component implements, or one it calls. It carries the bal openapi invocation for each, and where the generated module has to land.

Working with GraphQL Contracts

Load graphql.md before writing a client for a GraphQL document — a dependency whose contract type is graphql. The client is written by hand: the reference carries the target-record shape graphql:Client binds and the test that proves it.

bal library

Run bal library --help before your first lookup, and follow the flow it describes. It reads a package off Central so you write signatures that exist rather than remembered ones. --help is the grammar — the verbs, their flags, the session to walk; each document prints the rules about its own contents. What follows is what you carry into every lookup.

Working with library

Preferences
  • ballerina/* is the standard library; every vendor or third-party connector is ballerinax/*.
  • Prefer a ballerinax/* connector over a trigger.* package covering the same events.
  • Balerina has libraries created for working with standards, search before creating your own implementation.
  • A trailing // Special Agent Note: mentions important informations, follow them. eg: Other package references.
  • Line one states the document's own length If it says · 535 lines and you filtered using head | tail for 150 which mean the other 385 are gone and the answer can be among them: re-run that call unfiltered before writing anything from it. The library cli is designed to be self contained. You can't judge without the whole output.
  • -r flag only covers dependant types in the own package. For the external package references, you might need to invoke bal library type if you need them.
  • Tool may return ## Next section: Those are suggestions for your next look up navigation tool. Use them as hints.
  • Tool might return errors, Handle accoringly based on the error message.

bal build

  • An import plus bal build resolves a package — no manual Dependencies.toml edit.
  • target/ is the incremental cache — leave it in place. A build that printed Generating executable is green; do not re-run bal build to confirm it.
  • Configurables are read at runtime, so env variables or Config.toml is not needed to build.
  • bal build does NOT run tests. Any package with a tests/ directory verifies with bal build && bal test — a green build says nothing about a test that guards a correctness invariant.

Who the caller is: the gateway's signed assertion

A service whose design.json sets exposesAPI.auth implements specs/design/components/<name>/openapi.yaml.

The gateway has already done the authorization. It validated the caller's token and checked the scope each operation declares in that same contract; a request that failed either never reached this process. So this service holds no operation → scope table. That bal openapi drops the contract's security block no longer matters: there is nothing to transcribe, and nothing to keep in sync.

What the gateway cannot do is prove to your code that a request came through it — any pod that can open a socket to this service can send whatever headers it likes. So it signs a JWT of the caller it authenticated and puts it on the upstream request, and this service verifies that signature against one certificate the platform publishes per environment.

From the App Path:

cp "$AEP_SKILLS_DIR/ballerina/assets/gateway_assertion.bal" gateway_assertion.bal

If $AEP_SKILLS_DIR is unset, copy from assets/ next to this skill's SKILL.md (the BFF mirrors that directory to .claude/skills/ballerina/). The file is verbatim — there is no line to edit.

Two edits, and nothing else:

  1. The generated service becomes interceptable:
    service http:InterceptableService / on ep0 {
        public function createInterceptors() returns AssertionInterceptor => new;
        // …generated resources, unchanged…
    }
    
  2. A resource that needs an identity takes http:RequestContext ctx and calls requireGatewayCaller(ctx), returning its http:Unauthorized as-is. Match on the side the model stores — userId for a directory id, requireCallerUsername(caller) for a login name:
    // GET /me/claims — the caller's rows, resolved through the assertion and
    // nothing the client sent. `ownerId` here stores a directory id.
    resource function get me/claims(http:RequestContext ctx) returns Claim[]|http:Unauthorized {
        GatewayCaller|http:Unauthorized caller = requireGatewayCaller(ctx);
        if caller is http:Unauthorized {
            return caller;
        }
        return store.byOwner(caller.userId);
    }
    // Same shape when the model stores a login name instead — the only change
    // is which side of the comparison the assertion supplies.
    //   string|http:InternalServerError name = requireCallerUsername(caller);
    //   if name is http:InternalServerError { return name; }
    //   return store.byManager(name);
    // GET /claims — every row; a different operation, guarded by claims:read-all
    resource function get claims() returns Claim[] {
        return store.all();
    }
    

Nothing goes in Ballerina.toml or Dependencies.toml: ballerina/crypto, ballerina/jwt and ballerina/os are distribution modules that bal build resolves from the imports.

Read identity from the verified caller, never from a header.

Rule Why
requireGatewayCaller(ctx) in any resource that needs an identity. The assertion is the only statement about the caller that a forged request cannot make.
Match the caller on the side the model stores — userId for a directory id, requireCallerUsername(caller) for a login name. A UUID compared to a name matches nothing and answers 200 [] — "you own nothing", not "who are you". An identity failure is never an empty list.
Never bind X-User-Id, X-User-Scopes, X-User-Groups or Authorization as resource header parameters. The first three are unsigned — the gateway sets them, and so can anyone else who reaches this pod directly. Authorization is stripped.
A security: [] resource reads NO identity at all and takes no ctx. The gateway serves a public operation with no assertion, so there is no caller to read there.
Compare a scope with the asset's hasScope. It compares whole handles. string:includes is the trap — it matches claims:read inside claims:read-all.
Never http:ServiceConfig { auth: … }. That is listener-side token validation against the identity provider — the coupling the assertion exists to remove.

hasScope is never an operation's only check. Whether the operation may be called at all was settled at the gateway, from the contract.

A single-row read is the same rule at the other end: when the caller holds only the own handle and the row belongs to someone else, return 404, not 403 — a caller who may not see a row may not learn it exists from a status code either (api-management owns the rule; this is its Ballerina shape).

Verify a package with gateway_assertion.bal in it:

bal build && bal test

Write the tests against a throwaway RSA keypair: mint an assertion with the private half and export all three of GATEWAY_ASSERTION_CERTIFICATE (from its self-signed certificate), GATEWAY_ASSERTION_ISSUER and GATEWAY_ASSERTION_HEADER, so nothing in the test talks to a gateway. Export them before bal test: without the full trio the interceptor takes its unverified fallback, so every assertion test fails for a reason the output does not name. Cover four cases — a valid assertion is accepted; one signed by a different key is a 401; one whose payload was edited after signing is a 401 (never an anonymous caller); and a security: [] resource answers 200 with no assertion at all.

Dockerfile

When the user asks for a container image — load dockerfile.md.