Developer Tools AWS

Graphweaver 2.24.1 on AWS User Guide

| Product: Graphweaver 2.24.1 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Graphweaver 2.24.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg developer and API tools listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Graphweaver is an open source Node.js and TypeScript framework that turns your data sources into one GraphQL API and generates an admin dashboard for it. The image ships a small, working Graphweaver project: two entities, Project and Task, stored in SQLite and seeded with demo rows, so you can browse the dashboard and call the API on first boot, then replace the entities with your own.

Upstream Graphweaver has no login unless you add its auth package, so anyone who can reach it can read and change your data. The cloudimg image makes it private and locks down the GraphQL endpoint:

  • HTTPS from the first boot. nginx serves Graphweaver on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS.
  • A password unique to your instance, on everything. HTTP Basic Auth is required for the user admin on the admin dashboard, its files and the GraphQL API. The password is generated on first boot and written to /var/lib/cloudimg/graphweaver-credentials.txt (root only); nginx stores only a bcrypt hash of it. No password is baked into the image.
  • Trusted documents enforced. Graphweaver 2.24's trusted documents feature is switched on: the API runs only operations that were baked in when the project was built (the dashboard's own operations and the examples in src/documents/), sent by document id. An arbitrary query string is refused, even with the password.
  • No GraphQL over GET, no cross site requests. /graphql accepts only POST; a GET (and so a mutation smuggled into a link or image tag) is refused. Requests with the content types a web form can send cross site (text/plain, form and multipart) are refused before anything runs, and a request whose Origin is another site is refused.
  • Query limits and no introspection. Schema introspection is off, field name suggestions are hidden, and GraphQL Armor limits aliases, tokens, depth and cost.
  • No metadata service access. The Graphweaver service and nginx are blocked from the EC2 instance metadata service at the kernel level.
  • Fail closed. Graphweaver and nginx refuse to start until first boot has generated the certificate and the password.
  • Patched dependencies, no build tools. Graphweaver's vulnerable dependencies (MikroORM, Fastify, protobufjs, OpenTelemetry) are raised to fixed versions in the same major versions. The server ships as one compiled bundle, so no node_modules directory is on the image, and Node.js 24 LTS ships without npm, pnpm or a compiler.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Graphweaver 2.24.1 (the demo project and its build in /opt/graphweaver/app), the SQLite database (/opt/graphweaver/data), Node.js 24.22.1 LTS (/opt/node), nginx, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows TCP 22 (SSH), TCP 443 (Graphweaver) and TCP 80 (redirects to HTTPS) from your own networks only.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Graphweaver 2.24.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, and a security group as above.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=graphweaver}]'

Step 2: Connect to your instance

Version SSH user
Graphweaver 2.24.1 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot generates the certificate and the password, then Graphweaver and nginx start:

systemctl is-active graphweaver nginx graphweaver-firstboot
sudo test -f /var/lib/cloudimg/graphweaver-firstboot.done && echo FIRSTBOOT_DONE

All three units are active and the first boot marker is present:

active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on the network; the Graphweaver server (MainThread) listens on 127.0.0.1 only:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
127.0.0.1:9001 users:(("MainThread"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))

Step 4: Get your URL and password

sudo cat /var/lib/cloudimg/graphweaver-credentials.txt

The file is readable by root only and holds the URLs, the user name and this instance's password:

GRAPHWEAVER_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
GRAPHWEAVER_GRAPHQL_URL=https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/graphql
GRAPHWEAVER_USERNAME=admin
GRAPHWEAVER_PASSWORD=<GRAPHWEAVER_PASSWORD>

The URL uses the instance's public DNS name from EC2 instance metadata. The certificate also names the public and private IP addresses, so https://<public-ip>/ works too. To serve Graphweaver under your own DNS name, see Step 10.

Step 5: Use the admin dashboard

Open the GRAPHWEAVER_URL in your browser. The certificate is self signed, so your browser asks you to confirm it the first time (or install your own certificate, Step 10). Sign in as admin with the password from Step 4.

The dashboard lists the data source (mikro-orm-sqlite) and its entities. Project shows the four seeded projects, with a filter for every field, Export to CSV and Create New Project:

The Graphweaver admin dashboard showing the Project entity from the mikro-orm-sqlite data source with four seeded projects

Task shows the six seeded tasks:

The Task entity list with six seeded tasks, their projects and statuses

Click a row to open it in the side panel, change a field and choose Save. The change is written to the SQLite database at once:

The Task detail panel for "Design new landing page" with the title, project, status, priority and assignee fields and the Save button

Every operation the dashboard sends is one of its own trusted documents, so the dashboard works normally with enforcement on. The Open Playground button opens Graphweaver's GraphQL playground, which needs schema introspection and free form queries; both are switched off on this image, so the playground cannot load the schema or run queries unless you turn trusted documents off (Step 9).

Step 6: Call the GraphQL API

The API is at /graphql and accepts POST requests with Content-Type: application/json. Because trusted documents are enforced, a client sends the id of a known operation instead of the query text. The example operations shipped with the project and their ids are in a manifest file:

python3 -c 'import json; [print(o["id"], o["name"], o["type"]) for o in json.load(open("/opt/graphweaver/app/dist/trusted-documents/cloudimg-examples.persisted-query-manifest.json"))["operations"]]'
9b2ade22d46d0b47f550e54eaee5e85e2f76656fc9faa1453d479f1970ead39c ListProjects query
b25f375afce269a145971d42abb14be9acf53601333daa8e41da4e4745ee2d60 ListTasks query
71f0692147a2bc90882f819bd7a44b4a989cfb33b61a7aa8628b945bf7b24b44 CreateTask mutation
ec4d30eb30c977965e819a1923bc6d667335233036aa777b4dc53eec3afb4794 UpdateTask mutation
4b723b26ec1e91b319f0b6468afdd82a037247ca81d8f3d5b2d47bd842d2ae58 DeleteTask mutation

The id goes in the standard persisted query extension. This runs ListProjects (the password is read from the credentials file and passed to curl on standard input, so it never appears in the process list):

sudo bash -c '
  F=/var/lib/cloudimg/graphweaver-credentials.txt
  P=$(grep "^GRAPHWEAVER_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^GRAPHWEAVER_URL=" $F | sed -E "s#^GRAPHWEAVER_URL=https://([^/]+)/.*#\1#")
  gq() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" \
           -H "Content-Type: application/json" --data "$1" "https://$H/graphql"; echo; }
  # ListProjects, by its trusted document id
  gq "{\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"9b2ade22d46d0b47f550e54eaee5e85e2f76656fc9faa1453d479f1970ead39c\"}}}" \
    | grep -o "\"name\":\"[^\"]*\""
'
"name":"Website Redesign"
"name":"Mobile App"
"name":"Data Migration"
"name":"API Gateway"

Mutations take their input as variables. This creates a task with CreateTask, then removes it again with DeleteTask:

sudo bash -c '
  F=/var/lib/cloudimg/graphweaver-credentials.txt
  P=$(grep "^GRAPHWEAVER_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^GRAPHWEAVER_URL=" $F | sed -E "s#^GRAPHWEAVER_URL=https://([^/]+)/.*#\1#")
  gq() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" \
           -H "Content-Type: application/json" --data "$1" "https://$H/graphql"; echo; }
  CREATE=71f0692147a2bc90882f819bd7a44b4a989cfb33b61a7aa8628b945bf7b24b44
  DELETE=4b723b26ec1e91b319f0b6468afdd82a037247ca81d8f3d5b2d47bd842d2ae58
  OUT=$(gq "{\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$CREATE\"}},\"variables\":{\"input\":{\"title\":\"Write the runbook\",\"project\":\"API Gateway\",\"status\":\"Todo\",\"priority\":\"Medium\",\"assignee\":\"Ada\"}}}")
  echo "$OUT" | grep -o "\"title\":\"[^\"]*\""
  ID=$(echo "$OUT" | grep -o "\"id\":\"[0-9]*\"" | cut -d\" -f4)
  gq "{\"extensions\":{\"persistedQuery\":{\"version\":1,\"sha256Hash\":\"$DELETE\"}},\"variables\":{\"filter\":{\"id\":\"$ID\"}}}"
'
"title":"Write the runbook"
{"data":{"deleteTask":true}}

From your own machine, call https://<your-host>/graphql the same way with your HTTP client's Basic Auth option. Apollo Client sends ids for you with a persisted query link built from the manifest file above.

Step 7: Check the security posture from the command line

Without a login the dashboard and the API are refused; port 80 redirects to HTTPS; the health check stays open for load balancers; and a request for a host name the instance does not serve is dropped without an answer:

H=$(sudo grep '^GRAPHWEAVER_URL=' /var/lib/cloudimg/graphweaver-credentials.txt | sed -E 's#^GRAPHWEAVER_URL=https://([^/]+)/.*#\1#')
curl -s -o /dev/null -w 'http://  -> %{http_code} %{redirect_url}\n' --resolve "$H:80:127.0.0.1" "http://$H/"
curl -sk -o /dev/null -w 'admin UI, no login -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/"
curl -sk -o /dev/null -w 'GraphQL POST, no login -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" -X POST -H 'Content-Type: application/json' --data '{}' "https://$H/graphql"
curl -sk -o /dev/null -w 'health -> HTTP %{http_code}\n' --resolve "$H:443:127.0.0.1" "https://$H/health"
curl -sk -o /dev/null -w 'unknown host -> %{http_code}\n' --resolve "evil.example:443:127.0.0.1" "https://evil.example/" || true
http://  -> 301 https://ec2-<public-ip-dashed>.<region>.compute.amazonaws.com/
admin UI, no login -> HTTP 401
GraphQL POST, no login -> HTTP 401
health -> HTTP 200
unknown host -> 000

000 means nginx closed the connection without answering, which is how it treats any host name it does not serve.

Even with the password, a free form query and introspection are refused, a GraphQL GET is refused (so a mutation cannot ride in a link), a text/plain POST (what a cross site HTML form can send) is refused before any resolver runs, and a POST whose Origin is another site is refused:

sudo bash -c '
  F=/var/lib/cloudimg/graphweaver-credentials.txt
  P=$(grep "^GRAPHWEAVER_PASSWORD=" $F | cut -d= -f2-)
  H=$(grep "^GRAPHWEAVER_URL=" $F | sed -E "s#^GRAPHWEAVER_URL=https://([^/]+)/.*#\1#")
  a() { printf "user = \"admin:%s\"\n" "$P" | curl -K - -sk --resolve "$H:443:127.0.0.1" "$@"; }
  echo "raw query: $(a -H "Content-Type: application/json" --data "{\"query\":\"{ projects { id name } }\"}" "https://$H/graphql" | grep -o "This API only accepts trusted documents")"
  echo "introspection: $(a -H "Content-Type: application/json" --data "{\"query\":\"{ __schema { types { name } } }\"}" "https://$H/graphql" | grep -o "introspection is not allowed")"
  a -o /dev/null -w "GraphQL GET (a mutation) -> HTTP %{http_code}\n" -H "apollo-require-preflight: 1" \
    "https://$H/graphql?query=mutation%7BdeleteTask(filter:%7Bid:1%7D)%7D"
  a -o /dev/null -w "text/plain POST -> HTTP %{http_code}\n" -H "Content-Type: text/plain" \
    --data "{\"query\":\"{ projects { id } }\"}" "https://$H/graphql"
  a -o /dev/null -w "POST from another site -> HTTP %{http_code}\n" -H "Origin: https://evil.example" \
    -H "Content-Type: application/json" --data "{}" "https://$H/graphql"
'
raw query: This API only accepts trusted documents
introspection: introspection is not allowed
GraphQL GET (a mutation) -> HTTP 405
text/plain POST -> HTTP 400
POST from another site -> HTTP 403

nginx keeps only a bcrypt hash of the password; the plain text is in the root only credentials file:

sudo sed -E 's/\$2y\$12\$.*/$2y$12$<bcrypt-hash>/' /etc/nginx/.graphweaver-htpasswd
sudo stat -c '%a %U:%G %n' /etc/nginx/.graphweaver-htpasswd /var/lib/cloudimg/graphweaver-credentials.txt
admin:$2y$12$<bcrypt-hash>
640 root:www-data /etc/nginx/.graphweaver-htpasswd
600 root:root /var/lib/cloudimg/graphweaver-credentials.txt

The certificate was generated on this instance and names its own addresses:

H=$(sudo grep '^GRAPHWEAVER_URL=' /var/lib/cloudimg/graphweaver-credentials.txt | sed -E 's#^GRAPHWEAVER_URL=https://([^/]+)/.*#\1#')
echo | openssl s_client -connect 127.0.0.1:443 -servername "$H" 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName
subject=CN = ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name:
    DNS:ec2-<public-ip-dashed>.<region>.compute.amazonaws.com, IP Address:<public-ip>, IP Address:<private-ip>, IP Address:127.0.0.1, DNS:localhost

Graphweaver and nginx cannot reach the instance metadata service; Graphweaver runs as its own user with no capabilities and only its database directory writable; and neither service starts until first boot has finished:

for u in graphweaver nginx; do echo "$u IPAddressDeny: $(systemctl show -p IPAddressDeny --value $u | tr ' ' '\n' | sort | xargs)"; done
systemctl show graphweaver -p User -p ProtectSystem -p ReadWritePaths -p NoNewPrivileges -p CapabilityBoundingSet
systemctl cat graphweaver nginx | grep -E '^ExecStartPre='
graphweaver IPAddressDeny: 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
nginx IPAddressDeny: 168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
CapabilityBoundingSet=
User=graphweaver
ReadWritePaths=/opt/graphweaver/data
ProtectSystem=strict
NoNewPrivileges=yes
ExecStartPre=+/usr/bin/test -f /var/lib/cloudimg/graphweaver-firstboot.done
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStartPre=/usr/bin/test -f /var/lib/cloudimg/graphweaver-firstboot.done
ExecStartPre=/usr/bin/test -s /etc/nginx/.graphweaver-htpasswd

Step 8: Build your own API

The project source is on the instance in /opt/graphweaver/app: src/backend/entities/ (the MikroORM entities), src/backend/schema/ (the Graphweaver entities that become the GraphQL types), src/backend/database.ts (the SQLite connection), src/documents/ (your trusted operations), graphweaver-config.js, and package.json with its pnpm-lock.yaml. The image runs only the build output, dist/, and carries no package manager, so build on a development machine (or a separate build instance) with Node.js 24 and pnpm:

scp -r ubuntu@<public-ip>:/opt/graphweaver/app ./graphweaver-app && cd graphweaver-app
corepack enable
pnpm install --frozen-lockfile
# edit src/backend/entities, src/backend/schema and src/documents, then:
VITE_GRAPHWEAVER_API_URL=/graphql pnpm run build
cp node_modules/node-sqlite3-wasm/dist/node-sqlite3-wasm.wasm dist/backend/

pnpm run build writes the server bundle (dist/backend/index.js), the dashboard (dist/admin-ui/) and the trusted document manifests (dist/trusted-documents/), and adds every operation in src/documents/*.graphql plus the dashboard's own operations to the allow list. Copy dist/ back over /opt/graphweaver/app/dist/, create the tables for any new entities in /opt/graphweaver/data/database.sqlite (MikroORM does not create them for you), and restart the service with sudo systemctl restart graphweaver. To connect PostgreSQL or MySQL instead of SQLite, add the driver to the project as the Graphweaver documentation describes.

Step 9: Turning trusted documents off (not recommended)

If you need free form queries, for example to use the playground while you develop, add a drop-in that sets GRAPHWEAVER_TRUSTED_DOCUMENTS=off and restart the service:

sudo systemctl edit graphweaver

Add these lines, save, then run sudo systemctl restart graphweaver:

[Service]
Environment=GRAPHWEAVER_TRUSTED_DOCUMENTS=off

The Basic Auth login, the POST only endpoint, the CSRF checks, the query limits and the metadata block stay in place, but anyone with the password can then run any operation. Remove the drop-in (sudo systemctl revert graphweaver) and restart to switch enforcement back on.

Step 10: Your own DNS name and certificate

Point a DNS record at the instance, then set the name Graphweaver is served as. This mints a new self signed certificate for the name, updates nginx, the cross site protection and the URLs in the credentials file, and keeps the name across reboots:

sudo graphweaver-set-hostname <your-domain>

To use a certificate from your own certificate authority instead, copy it over the generated pair and reload nginx (running graphweaver-set-hostname again replaces it with a self signed one):

sudo install -m 0644 <your-domain>.crt /etc/ssl/graphweaver/graphweaver.crt
sudo install -m 0600 <your-domain>.key /etc/ssl/graphweaver/graphweaver.key
sudo nginx -t && sudo systemctl reload nginx

Step 11: Change the password or add users

The login is an nginx Basic Auth file. To set a new password for admin, hand it to htpasswd on standard input (printf is a shell builtin, so the password never appears in the process list; start the line with a space so most shells keep it out of your history), then reload nginx:

 printf '%s\n' '<new-password>' | sudo htpasswd -i -B -C 12 /etc/nginx/.graphweaver-htpasswd admin
sudo systemctl reload nginx

Running sudo htpasswd -B -C 12 /etc/nginx/.graphweaver-htpasswd <user> without -i prompts for the password instead. Use it with another user name to give a colleague their own login, and sudo htpasswd -D /etc/nginx/.graphweaver-htpasswd <user> to remove one. Then update or delete GRAPHWEAVER_PASSWORD in /var/lib/cloudimg/graphweaver-credentials.txt so it does not hold an old password.

Step 12: Server components

Component Path
Project source and build output (dist/) /opt/graphweaver/app
SQLite database /opt/graphweaver/data/database.sqlite
Node.js 24 LTS runtime (no npm, no pnpm) /opt/node
Build record, bundle package list, dependency audit, pristine database /usr/share/doc/cloudimg-graphweaver/
Graphweaver service graphweaver.service (user graphweaver, 127.0.0.1:9001)
nginx site /etc/nginx/sites-available/cloudimg-graphweaver
Basic Auth file /etc/nginx/.graphweaver-htpasswd (bcrypt)
Certificate and key /etc/ssl/graphweaver/
First boot script /usr/local/sbin/graphweaver-firstboot.sh
Host name helper /usr/local/sbin/graphweaver-set-hostname
Credentials /var/lib/cloudimg/graphweaver-credentials.txt (mode 0600)
cat /opt/graphweaver/VERSION
/opt/node/bin/node --version
ls /opt/node/bin
command -v npm npx pnpm || echo "no npm, npx or pnpm"
ls /opt/graphweaver/app/dist/backend
nginx -v 2>&1
graphweaver_version=2.24.1
node_version=v24.22.1
v24.22.1
node
no npm, npx or pnpm
index.js
node-sqlite3-wasm.wasm
nginx version: nginx/1.24.0 (Ubuntu)

The server bundle's package list and its npm advisory audit are recorded on the image. One advisory pair remains, in a second copy of @graphql-tools/utils (11.0.0) that Graphweaver uses only to print its schema; the affected functions are never called, and there is no fixed release in that major version yet:

python3 -c 'import json; d=json.load(open("/usr/share/doc/cloudimg-graphweaver/npm-audit-bundle.json")); print(d["packages"], "packages in the server bundle;", len(d["advisories"]), "advisories, unheld:", len(d["unheld"])); [print(" ", a["name"], a["installed"], a["url"].rsplit("/",1)[-1], "held (d-129, unreachable)") for a in d["advisories"]]'
python3 -c 'import json; d=json.load(open("/usr/share/doc/cloudimg-graphweaver/d129-graphql-tools-utils.json")); print("graphweaver uses:", d["functionsUsed"], "reachable:", d["reachable"])'
222 packages in the server bundle; 2 advisories, unheld: 0
  @graphql-tools/utils ['11.0.0', '12.1.0'] GHSA-7mx3-vvmw-hjmv held (d-129, unreachable)
  @graphql-tools/utils ['11.0.0', '12.1.0'] GHSA-c4qx-w738-7hr3 held (d-129, unreachable)
graphweaver uses: ['printSchemaWithDirectives'] reachable: False

To return to the starting data, stop the service, copy /usr/share/doc/cloudimg-graphweaver/demo-database.sqlite over /opt/graphweaver/data/database.sqlite (owner graphweaver, mode 0640) and start it again. The service logs warnings and errors only (journalctl -u graphweaver), so your queries and their data are not written to the system journal.

Step 13: Security recommendations

  • Allow ports 22, 80 and 443 in the security group from your own networks only.
  • Keep the per instance password secret, and rotate it if it may have been shared (Step 11).
  • Use your own DNS name and a certificate your clients trust (Step 10).
  • Keep trusted documents on in production and add the operations your clients need to src/documents/ (Step 8).
  • Back up /opt/graphweaver/data/database.sqlite, or move to a managed database for production data.
  • Keep the operating system patched; unattended security upgrades are on, and sudo apt-get update && sudo apt-get upgrade applies everything else.

Support and licensing

Graphweaver is open source under the Apache License 2.0 (/opt/graphweaver/app/LICENSE.md); there is no per user or per CPU licence fee. The AMI price covers cloudimg's hardening, first boot setup and support. cloudimg is not affiliated with or endorsed by Exogee or the Graphweaver project.

  • Email: support@cloudimg.co.uk
  • Website: https://www.cloudimg.co.uk
  • Support: 24/7, with a 24 hour response SLA