Deployment guide

Graphweaver 2.24.1

Overview

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. This guide covers the Graphweaver 2.24.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud developer and API tools listing on AWS Marketplace, and you pick it as the software version when you subscribe and launch.

The image ships a small, working Graphweaver project with two entities, Project and Task, stored in SQLite and seeded with demo rows. 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 SecureAnyCloud image makes it private and locks down the GraphQL endpoint.

What you get:

  • 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/sac/data/graphweaver-credentials.txt (root only). nginx stores only a bcrypt hash of it, and no password is baked into the image.
  • Trusted documents enforced. The Graphweaver 2.24 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, so a GET (and with it 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. A request whose Origin is another site is also 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 within 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.
  • 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.
  • Defaults: 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
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 select your Region. Then choose Launch through EC2.

Choose m5.large, your key pair, and a security group as described in the prerequisites.

Alternatively, launch 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}]'
2

Connect to your instance

Connect over SSH as the ubuntu user, which is the SSH user for Graphweaver 2.24.1 on Ubuntu 24.04:

ssh -i <your-key.pem> ubuntu@<public-ip>
3

Check the services

First boot generates the certificate and the password, and then Graphweaver and nginx start. Check the three units and the first-boot marker:

systemctl is-active graphweaver nginx graphweaver-firstboot
sudo test -f /var/lib/sac/data/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"))
4

Get your URL and password

Read the credentials file:

sudo cat /var/lib/sac/data/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.

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. You can also 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. Task shows the six seeded tasks.

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.

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 the Graphweaver 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).

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. List them:

python3 -c 'import json; [print(o["id"], o["name"], o["type"]) for o in json.load(open("/opt/graphweaver/app/dist/trusted-documents/sac-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 command runs ListProjects. It reads the password from the credentials file and passes it to curl on standard input, so the password never appears in the process list:

sudo bash -c ' F=/var/lib/sac/data/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 command creates a task with CreateTask and then removes it again with DeleteTask:

sudo bash -c ' F=/var/lib/sac/data/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, using your HTTP client's Basic Auth option. Apollo Client sends the ids for you with a persisted query link built from the manifest file above.

7

Check the security posture from the command line

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

Run these checks:

H=$(sudo grep '^GRAPHWEAVER_URL=' /var/lib/sac/data/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, several requests are refused. 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 so is a POST whose Origin is another site.

Test each of them:

sudo bash -c ' F=/var/lib/sac/data/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/sac/data/graphweaver-credentials.txt
admin:$2y$12$<bcrypt-hash> 640 root:www-data /etc/nginx/.graphweaver-htpasswd 600 root:root /var/lib/sac/data/graphweaver-credentials.txt

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

H=$(sudo grep '^GRAPHWEAVER_URL=' /var/lib/sac/data/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 = sac 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. Neither service starts until first boot has finished.

Confirm the service restrictions:

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/sac/data/graphweaver-firstboot.done ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;' ExecStartPre=/usr/bin/test -f /var/lib/sac/data/graphweaver-firstboot.done ExecStartPre=/usr/bin/test -s /etc/nginx/.graphweaver-htpasswd
8

Build your own API

The project source is on the instance in /opt/graphweaver/app:

  • src/backend/entities/ holds the MikroORM entities
  • src/backend/schema/ holds the Graphweaver entities that become the GraphQL types
  • src/backend/database.ts holds the SQLite connection
  • src/documents/ holds your trusted operations
  • graphweaver-config.js holds the Graphweaver configuration
  • package.json comes with its pnpm-lock.yaml

The image runs only the build output, dist/, and carries no package manager. 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/). It also 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, because MikroORM does not create them for you. Then 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.

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. Open the drop-in editor:

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. However, anyone with the password can then run any operation.

To switch enforcement back on, remove the drop-in with sudo systemctl revert graphweaver and restart the service.

10

Your own DNS name and certificate

Point a DNS record at the instance, then set the name that Graphweaver is served as. This command mints a new self-signed certificate for the name. It also updates nginx, the cross-site protection and the URLs in the credentials file, and it 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 certificate.

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
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, and then reload nginx. printf is a shell builtin, so the password never appears in the process list. Start the line with a space so that most shells keep it out of your history.

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. Use sudo htpasswd -D /etc/nginx/.graphweaver-htpasswd <user> to remove a login.

Then update or delete GRAPHWEAVER_PASSWORD in /var/lib/sac/data/graphweaver-credentials.txt so that the file does not hold an old password.

12

Server components

These are the components and where they live on the instance:

  • 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 and pristine database: /usr/share/doc/sac-graphweaver/
  • Graphweaver service: graphweaver.service (user graphweaver, 127.0.0.1:9001)
  • nginx site: /etc/nginx/sites-available/sac-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/sac/data/graphweaver-credentials.txt (mode 0600)

Check the installed versions and the runtime:

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.

Read the audit records:

python3 -c 'import json; d=json.load(open("/usr/share/doc/sac-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/sac-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/sac-graphweaver/demo-database.sqlite over /opt/graphweaver/data/database.sqlite (owner graphweaver, mode 0640), and start the service again.

The service logs warnings and errors only (journalctl -u graphweaver), so your queries and their data are not written to the system journal.

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), with no per-user or per-CPU licence fee. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support. SecureAnyCloud is not affiliated with or endorsed by Exogee or the Graphweaver project.

  • Email: sales@secureanycloud.com
  • Website: https://secureanycloud.com
  • Support: 24/7, with a 24-hour response SLA

Deploy on AWS

Launch Graphweaver 2.24.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS