hecks

Your own domain

Getting started ran a domain that ships in the repository. This guide is the next step: you write a domain of your own, run it, and deploy it to AWS Lambda so that something other than Ruby can call it.

The domain is small on purpose: books on a shelf, lent to one borrower at a time. Every step is a command you type from the root of a clone of the repository (the README quickstart gives the clone and bundle install; stay in that hecks directory, since bundle exec needs it).

Two shell settings come first. hecks records each command it runs in a journal, which by default lives in a local Postgres. Setting HECKS_ENVIRONMENT=memory keeps that journal in memory, so nothing here needs a database until you deploy:

export HECKS_ENVIRONMENT=memory

hecks console and hecks init set this for themselves; the other commands below do not. Skip it and hecks deploy project stops with cannot bind PostgresEra, which means hecks went looking for that local Postgres.

1. Write it

A domain lives in a directory that is not inside the clone, and the layout matters: a bluebook folder holding a .bluebook file named after the domain, with its .world beside it. hecks init writes that layout for you as a stub:

bundle exec hecks init Lending --dir="$HOME/lending" --adapter=Postgres

It writes lending.bluebook, lending.world and an environments/memory.world, tells you what to type next, and never replaces a file that is already there. (If you would rather start from a conversation with someone who knows the business, hecks interview Lending asks the questions, keeps what the expert says, and drafts the domain from what you accept. It sends the answers to a model through your own claude login; --no-ai leaves that out.) --adapter says where the data will live; Postgres is what the AWS Lambda host serves, which is why it is chosen here (leave it out and you get SQLite, which needs no server). The stub boots as it is, with a placeholder aggregate called Example.

Replace the contents of $HOME/lending/bluebook/lending.bluebook with this:

```ruby bluebook Hecks.bluebook “Lending” do vision “Lend books from a shelf to one borrower at a time.” supporting

aggregate “Book” do description “A book that is on the shelf or lent to a borrower.”

identified_by :isbn

attribute :isbn,     Isbn
attribute :title,    Title
attribute :borrower, Borrower

value_object "Isbn" do
  attribute :value, String, matches: :present
end

value_object "Title" do
  attribute :value, String, matches: :present
end

value_object "Borrower" do
  attribute :value, String, matches: :present
end

lifecycle :status, default: "shelved" do
  transition "Lend"   => "lent",    from: "shelved"
  transition "Return" => "shelved", from: "lent"
end

command "Shelve" do
  goal "Add a book to the shelf"

  attribute :isbn,  Isbn
  attribute :title, Title

  emits "BookShelved"
end

command "Lend" do
  goal "Lend a shelved book to a borrower"

  reference_to Book
  attribute :borrower, Borrower
  sets :borrower

  emits "BookLent"
end

command "Return" do
  goal "Take a lent book back"

  reference_to Book

  emits "BookReturned"
end   end end ```

Read it top to bottom; it uses only words you met in Getting started:

Ask hecks to read it back before running anything:

bundle exec hecks docs "$HOME/lending/bluebook"

hecks docs prints each command’s arguments, the lifecycle, and every refusal the domain can produce. If you made a mistake in the file, this is where it is named.

2. Run it

Where the data lives is a separate file, because it is a decision about a deployment and not about the domain. hecks init wrote it as $HOME/lending/bluebook/lending.world:

Hecks.world "Lending" do
  realm "Lending"
  default_adapter "Postgres"
  default_database "postgres://localhost/lending"
end

default_adapter binds every aggregate in one line, so this domain needs no .hecksagon file. It names Postgres because that is what runs on Lambda. To run on your laptop without a database, init also wrote an overlay that swaps the adapter, as $HOME/lending/bluebook/environments/memory.world:

Hecks.world "Lending" do
  default_adapter "Memory"
end

HECKS_ENVIRONMENT=memory selects that overlay, which is why you exported it. Now open the domain:

bundle exec hecks console subject="$HOME/lending"

The console lists Book: lend!, return!, shelve! and gives you a prompt. Type:

book = Book.shelve!(isbn: "978-0", title: "Dune")
book.status                        # => "shelved"

book.lend!(borrower: "Ana")
book.status                        # => "lent"
book.borrower.to_h                 # => { value: "Ana" }

A book that is already lent cannot be lent again. The refusal comes from the lifecycle and names the state:

book.lend!(borrower: "Ben")        # ~> LifecycleRefused: Lend refused — status is "lent", and Lend moves it only from "shelved"

Return it and the shelf is whole again:

book.return!
book.status                        # => "shelved"
book.events.map(&:name)            # => ["BookShelved", "BookLent", "BookReturned"]

That is the whole loop: change the .bluebook, run it, see what it accepts and refuses. Do this until the rules are the ones you meant, because the deployed service enforces exactly these.

3. Deploy it to Lambda

Lambda runs the domain as a function behind your AWS account’s own sign-in: callers authenticate with AWS credentials (an IAM-signed request), so there is no password or session of yours to build first.

You need an AWS account with credentials configured, the aws and sam command-line tools, cargo-lambda, and Rust installed through rustup. The Rust tree in the clone is what gets compiled, so the first build is slow. If the build reports a missing target, rustup target add wasm32-wasip1 adds the WebAssembly one (the build adds the Lambda target itself).

First say where it deploys. Add this inside Hecks.world "Lending" do ... end in lending.world; init leaves it out on purpose, since it commits you to a region:

  deployed_to("AwsLambda") do
    region "us-east-1"
    memory 512
    timeout 10
    database "Postgres"
  end

Then turn the domain into a deployable stack. This writes files and touches nothing in AWS:

bundle exec hecks deploy project "$HOME/lending" --out="$HOME/lending-deploy"

It writes template.yaml, samconfig.toml, bastion.yaml and a Makefile. The stack is named hecks-lending and the function hecks-lending, after the domain. Read template.yaml before you deploy. It creates, in your account:

The RDS instance is the part that costs money while it exists. Then deploy:

cd "$HOME/lending-deploy"
export AWS_REGION=us-east-1
make deploy

make deploy builds the function (sam build), checks the compiled rules against a corpus if you wrote one, creates the stack (sam deploy), then runs make mint-era, which opens a short-lived tunnel to the new database and records the domain’s first shape in it. The Makefile contains the absolute paths of your clone and of $HOME/lending, so generate it on the machine that deploys, and generate it again if either path moves.

Call it

Any client that can sign an AWS request can call it; the AWS command line is the shortest. Read the whole state:

aws lambda invoke --function-name hecks-lending --region us-east-1 \
  --cli-binary-format raw-in-base64-out \
  --payload '{"read": true}' out.json && cat out.json

Issue a command. verb is <Domain>::<Aggregate>.<Command>, and with holds the command’s facts in the shape hecks docs printed:

aws lambda invoke --function-name hecks-lending --region us-east-1 \
  --cli-binary-format raw-in-base64-out \
  --payload '{"verb": "Lending::Book.Shelve", "with": {"isbn": {"value": "978-0"}, "title": {"value": "Dune"}}}' \
  out.json && cat out.json

The answer is always a document with refusals (empty when the command was accepted), events and instances. A refused command still comes back as a successful call, so read refusals. The other languages use the same payload through their AWS SDK’s Lambda Invoke.

To remove it, delete the hecks-lending stack. Delete the leftover database snapshot too if you want no residue.

What was checked

Every Ruby block in this guide runs under spec/guides_spec.rb. The same domain files were also run directly: hecks docs, a scripted boot on Memory and hecks console subject= all worked. hecks deploy project wrote the four files above for the Lambda target, and sam build in that directory compiled the domain into the function (a bootstrap binary, lending.wasm and lending.ir.json).

Not run: sam deploy, make deploy and the aws lambda invoke calls, so the deploy section is derived from the generated files and from Running a rules service, which is the full procedure and lists its known gaps. Read those before you rely on a stack, in particular how the function reads its database password at start-up (it fetches it from Secrets Manager and the generated template gives the function no route out of its VPC), and how clients are authenticated when more than one caller needs different permissions.