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.mdskills/ballerina/SKILL.md
GitHubBallerina
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>
- Read project-structure.md for how a Ballerina project is laid out.
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 isballerinax/*.- Prefer a
ballerinax/*connector over atrigger.*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 linesand 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 typeif 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
importplusbal buildresolves a package — no manualDependencies.tomledit. target/is the incremental cache — leave it in place. A build that printedGenerating executableis green; do not re-runbal buildto confirm it.- Configurables are read at runtime, so env variables or Config.toml is not needed to build.
bal builddoes NOT run tests. Any package with atests/directory verifies withbal 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:
- The generated service becomes interceptable:
service http:InterceptableService / on ep0 { public function createInterceptors() returns AssertionInterceptor => new; // …generated resources, unchanged… } - A resource that needs an identity takes
http:RequestContext ctxand callsrequireGatewayCaller(ctx), returning itshttp:Unauthorizedas-is. Match on the side the model stores —userIdfor 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.