Sign inSign up

husseinakar/antipode

By husseinakar

•Updated 11 days ago

A stateful stand-in for every external system your service calls, and the channels that call it.

Image
0

3.1K

husseinakar/antipode repository overview

⁠Antipode

Antipode — Gain Architectural Control

The world your service thinks is real.

Your service calls other systems: a ledger, a customer directory, a stock service, a partner's API. Antipode plays all of them, on one port, and remembers what happened. Take a payment and the balance goes down. Ask for the balance and you get the new number. Retry with the same idempotency key and the money still leaves only once.

It plays the other direction too. Webhooks, Kafka records and RabbitMQ, SQS or SNS messages go into your service over the real transport, at the press of a button. You build the whole world in your browser, or a coding agent writes it for you from your service's code.

The console: channels that call your service on the left, your service in the middle, the systems it calls on the right

⁠Run it

docker run -p 8090:8090 --name antipode \
  -v "$PWD/antipode:/app/config" \
  --add-host host.docker.internal:host-gateway \
  husseinakar/antipode
PartWhy it is there
-p 8090:8090Your browser and your service reach Antipode here.
-v "$PWD/antipode:/app/config"Your world is saved in antipode/antipode.json on your disk, so it outlives the container and can be committed.
--add-host host.docker.internal:host-gatewayLets Antipode reach your service on your machine. Docker Desktop has this built in; Linux needs the line.

Then open:

The designer, where you build the worldhttp://localhost:8090/__admin/designer⁠
The console, where you use ithttp://localhost:8090/⁠

⁠Take the tour

  1. Start the demo service that stands in for yours, from the same image:

    docker run --rm -p 8080:8080 --add-host host.docker.internal:host-gateway \
      -e ANTIPODE_URL=http://host.docker.internal:8090 \
      husseinakar/antipode node examples/demo-service/service.mjs
    
  2. In the designer, click Template, then Presets. Under Reference world, press Load it and confirm.

  3. Press Console →. Your service is in the middle: today that is the demo service. The channels that call it are on the left, and the systems it calls are on the right.

  4. Hover the Order walk card and press ▶. One order goes in, and six calls go out to the systems on the right.

  5. Hover Reservations and press its list button, what it holds. The hold that order placed is in the table.

  6. Press the power button on Reservations and choose once and 503 Unavailable. Send another order: it stops at Reservations. All live in the top bar clears every break.

⁠Use it with your service

1. Run it from your service's repository, with the command above. The antipode/ folder there is Antipode's config folder from now on.

2. Generate the world from your code. A skill for coding agents reads your Feign clients, WebClient and RestTemplate calls, controllers, listeners and OpenAPI document, and writes antipode/antipode.json. Install it into your service's repository:

mkdir -p .claude/skills/antipode-config
curl -s localhost:8090/__admin/skill \
  -o .claude/skills/antipode-config/SKILL.md

Then run /antipode-config in Claude Code. The file is plain Markdown, so Cursor, Gemini CLI and any agent that reads AGENTS.md can follow it too.

3. Check it in the designer. Because the folder is mounted, the new world is already loaded: refresh the designer. Open each system, use Try it on its endpoints, and Save. A config from somewhere else goes in through Template → Import. You can also build the world by hand: drag External service and Inbound channel onto the map.

4. Point your service at Antipode. Set the base URL of every system your service calls to http://localhost:8090. Antipode tells them apart by path. In the designer, Your service → Base URL is where channels deliver to your service: http://host.docker.internal:8080 by default.

5. Commit antipode/antipode.json, and add antipode/history/ to .gitignore. A colleague who runs the same command gets the same world.

⁠What you can do in the console

Hover a card to see its buttons.

  • On a system: see and edit what it holds, Reset to seed, break it with a status of your choice, or slow it down, for one call or until you stop.
  • On a channel: fill in the form, choose how many to send, from Once to ×1000, and press ▶. A field can invent its value, such as a name or an amount, so a hundred deliveries are a hundred different documents.
  • In the top bar: All live clears every break, Reset counts zeroes the counters.

⁠Folders

PathWhat it holdsMount it?
/app/configantipode.json, its earlier versions, uploaded filesYes, to keep your world
/app/dataEach system's live dataOnly to keep data between containers
/app/behaviorsBehaviour packs, JavaScript for answers a config can't giveOnly if you have one

On Linux, the container runs as user 1000, and a bind mount keeps your folder's owner. Make the folder writable by that user, or add --user "$(id -u):$(id -g)".

⁠In your Docker Compose file

services:
  antipode:
    image: husseinakar/antipode:latest
    ports: ['8090:8090']
    volumes:
      - ./antipode:/app/config
    extra_hosts: ['host.docker.internal:host-gateway']

Keep extra_hosts if you start your service from your IDE, outside Compose. Antipode's data stops with the project, so a standalone container is better when your app restarts often. It can still join the project's network: docker network connect your-project_default antipode.

⁠Brokers

Set each broker's address on its transport, under Transports in the designer.

AddressStandalone containerIn your Compose project
Antipode, in your service's base URLshttp://localhost:8090http://antipode:8090
Your service, in Base URLhttp://host.docker.internal:8080 ᵈhttp://your-app:8080
Kafka, in the transport's brokershost.docker.internal:9092kafka:9092 ᵈ
RabbitMQ, in the transport's urlhttp://host.docker.internal:15672http://rabbitmq:15672 ᵈ
SQS or SNS, in the transport's endpointhttp://host.docker.internal:4566http://localstack:4566 ᵈ

ᵈ is the default.

  • A topic or queue that does not exist yet is created on the first send, on Kafka, SQS and SNS. A RabbitMQ queue is declared by your service, so start it before sending.
  • RabbitMQ is reached on port 15672, its management API, not on 5672. Use the rabbitmq:management image and a real user: RabbitMQ only lets guest in from localhost.
  • SQS and SNS work against real AWS, LocalStack and ElasticMQ. Leave the credentials empty and pass AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY to the container. A Protobuf or Avro message travels as base64 in the body, with nothing in front of it.
  • Kafka is spoken directly on port 9092. A single-node broker that advertises localhost:9092 works from inside the container.
  • Avro, Protobuf and JSON Schema: copy your service's .avsc or .proto into antipode/schemas/, imports included, or add it under Schemas in the designer. Point your service's schema.registry.url at http://localhost:8090/__registry.

⁠Environment variables

VariableWhat it does
ANTIPODE_PORTThe port. Default 8090
ANTIPODE_MODEmock answers calls, proxy sends them to the real systems
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYCredentials for SQS and SNS

⁠More

The full guide, with the designer, the console, proxy mode and behaviour packs: https://github.com/hussein-akar/antipode⁠

Tags: latest follows the main branch. Each release is also published as its version, such as 1.9.0 and 1.9. Built for linux/amd64 and linux/arm64.

Licence: Apache License 2.0⁠.

Tag summary

Content type

Image

Digest

sha256:f25e09f5d…

Size

58.9 MB

Last updated

11 days ago

docker pull husseinakar/antipode