Self-explanatory showcase: what the template contains, how everything is connected, how it is deployed, plus a working demo against the database.
A single repo contains the static frontend, the Express API and the infrastructure as code. Vercel serves both (frontend + API) as a free serverless function; data lives in an AWS DynamoDB table in pay-per-request mode (zero cost with no traffic). GitHub Actions deploys the infrastructure.
flowchart LR
subgraph Browser
UI[Static showcase
public/]
end
subgraph Vercel["Vercel (free)"]
FN["Serverless function
src/server.js → Express"]
end
subgraph AWS["AWS (eu-west-1)"]
DDB[("DynamoDB
table vedtemplate-app
pk / sk")]
end
subgraph GitHub
GHA[["Actions:
ci.yml + deploy-infra.yml"]]
CFN["CloudFormation
infra/dynamodb.yml"]
end
UI -- "fetch /api/*" --> FN
FN -- "AWS SDK v3
(scoped IAM credentials)" --> DDB
GHA -- "aws cloudformation deploy" --> CFN
CFN -- "creates/updates" --> DDB
GHA -. "push to main
runs tests" .-> FN
sequenceDiagram
participant B as Browser
participant V as Vercel (Express)
participant S as itemsService
participant D as DynamoDB
B->>V: POST /api/items {text}
V->>S: createItem(text)
alt AWS credentials configured
S->>D: PutCommand {pk: "ITEM", sk: date#id, ...}
D-->>S: OK
else no credentials (memory mode)
S->>S: push to in-RAM array
end
S-->>V: item {id, text, createdAt}
V-->>B: 201 Created + JSON
The whole app uses one single table. The pk prefix discriminates
the entity; adding a new entity requires no infrastructure change, just a new service.
| pk | sk | attributes | purpose |
|---|---|---|---|
ITEM | <isoDate>#<uuid> | id, text, createdAt | Demo records (chronological order for free via Query) |
| YOUR_ENTITY | whatever you need | … | Add the prefix in src/config/dynamo.js and create a service |
flowchart TD
R[repo] --> SRC[src/ · Express backend]
R --> PUB[public/ · this showcase]
R --> INF[infra/dynamodb.yml · CloudFormation]
R --> SCR[scripts/ · setup-aws.sh, seed.js]
R --> GHW[.github/workflows/ · ci.yml, deploy-infra.yml]
R --> TST[tests/ · node:test, no AWS]
SRC --> A[app.js · middleware + routes]
SRC --> C[config/dynamo.js · single client + pk prefixes]
SRC --> RT[routes/ · items, status]
SRC --> SV[services/ · logic + memory fallback]
express.static), so there is no CORS in production and only one pipeline.DeletionPolicy: Retain — deleting the stack never deletes data.
A monitoring, auditing and visitor-identification tool built into the app itself. It
answers the questions an owner actually cares about: how many people visit, who they
are, where from, whether they are real humans or bots, and whether traffic is trending.
Everything below is live from GET /api/traffic — try the buttons.
Do-Not-Track is honoured.
Each visitor is a browser fingerprint (canvas + non-PII signals, hashed). It lets us count unique people and spot returning ones without a login.
flowchart LR V[Visitor browser] -- "page load" --> BE[track.js beacon
fingerprint + signals] BE -- "POST /api/traffic/track" --> FN[Express function] A[Any API call] --> MW[access-log middleware] MW --> FN FN -- "mask IP · hash · redact sensitive
parse UA · detect bot · geo (edge)" --> EV[(DynamoDB
EVENT · TTL 7d)] FN --> VP[(DynamoDB
VISITOR profile)] EV -- "GET /api/traffic
aggregate on read" --> DASH[This dashboard] VP --> DASH
Data served by GET /api/status: the backend checks its environment variables and performs a real DescribeTable against DynamoDB.
…
Every action calls the real API (/api/items). If the status says
dynamodb, this writes to your AWS table; if it says memory,
it writes to the server's RAM. The code is identical in both cases.
Tip: open this page in two tabs and add a record in one; refresh the other to see the shared persistence.
flowchart LR DEV[git push to main] --> CI[GitHub Actions: ci.yml
npm ci + npm test] DEV --> VC[Vercel: automatic build + deploy] DEV -- "if infra/** changes" --> INF[GitHub Actions: deploy-infra.yml
cloudformation deploy] INF --> TBL[(DynamoDB table)] VC --> PROD[App in production] TBL -. "credentials in env vars" .-> PROD
npm install npm test # 6 tests, memory mode npm run dev # http://localhost:3000
gh repo create user/my-app --public --source . --push) and push.
The ci.yml workflow already passes green without any secret.
💡 You can mark it as a Template repository (Settings →
General): repos generated with "Use this template" copy everything, including the CI
workflows, which work the same. The only thing not inherited are the secrets:
in each generated repo repeat the setup-github-secrets.sh step and change
STACK_NAME/TABLE_NAME in deploy-infra.yml if you
want one table per app.
vercel.json and publishes. The app already works, in memory mode.
# A) Via pipeline (recommended): upload the secrets and trigger the workflow ./scripts/setup-github-secrets.sh --profile myprofile --repo user/repo gh workflow run deploy-infra.yml # B) Manually from your machine aws cloudformation deploy --template-file infra/dynamodb.yml \ --stack-name vedtemplate-app --profile myprofile --region eu-west-1
./scripts/setup-aws.sh --profile myprofile # Paste the 4 variables it prints into Vercel → Settings → Environment Variables # and redeploy. The "Connections" tab will turn green.
cp .env.example .env # paste the same credentials there npm run seed
This instance (vedtemplate.infranettone.com) was deployed entirely from the terminal. There were only two manual authorizations, one click each: the device-code login (gh and vercel) and installing Vercel's GitHub App on the organization for auto-deploys.
# 1) Public GitHub repo, marked as template
gh repo create infranettone/infranettone-template-vercel-express-dynamodb --public --source . --push
gh api -X PATCH repos/OWNER/REPO -f is_template=true
# 2) Vercel project + first deploy (already works, in memory mode)
vercel link --yes --project <name> && vercel deploy --prod --yes
# 3) Make the site public: Deployment Protection is ON by default
# PATCH /v9/projects/<id>?teamId=<team> {"ssoProtection": null}
# 4) Auto-deploy: install github.com/apps/vercel on the org and...
vercel git connect --yes
# 5) AWS infra via pipeline (deploy IAM user + secrets + stack + table)
./scripts/setup-github-secrets.sh --profile <profile> --repo OWNER/REPO
gh workflow run deploy-infra.yml
# 6) Runtime credentials → Vercel env vars via CLI → redeploy
./scripts/setup-aws.sh --profile <profile>
printf '<value>' | vercel env add AWS_SECRET_ACCESS_KEY production --sensitive
vercel redeploy <url>
Gotchas already fixed in the template: Node 20 does not expand globs in node --test;
Vercel truncates the *.vercel.app domain for long names; Vercel sets its own
AWS_REGION at runtime; repos generated from a template do not inherit GitHub secrets.
| Variable | Where | Purpose |
|---|---|---|
AWS_REGION | Vercel + .env | Table region (eu-west-1) |
DYNAMODB_TABLE | Vercel + .env | Table name |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | Vercel + .env | IAM user scoped to the table (setup-aws.sh) |
CORS_ORIGINS | optional | Only if another site calls this API |
AWS_ACCESS_KEY_ID/SECRET (GitHub secrets) | repo | Pipeline deploy user (setup-github-secrets.sh) |
SEO (Search Engine Optimization) is the craft of helping search engines understand and trust your page so it appears when people search for what you offer. You don't pay Google to appear here — you make the page easy to read for both humans and crawlers. Here is the plain-English version, and exactly what this template already does for you.
flowchart LR C[1 · Crawl
Googlebot fetches your pages
following links + sitemap.xml] --> I[2 · Index
reads title, description,
headings, structured data] I --> R[3 · Rank
shows your page for queries
by relevance + trust] R --> U[Someone searches
and finds you]
<title> and meta description — the blue headline and grey text you see in Google results. Written around what the site is about.robots.txt + sitemap.xml — served at the site root, they invite crawlers in and list the pages to index.| Method | Path | Description |
|---|---|---|
| GET | /api/status/health | Instant health check (for monitors) |
| GET | /api/status | Full status: env, runtime, DynamoDB with latency |
| GET | /api/items | Lists records (max 100, newest first) |
| POST | /api/items | Creates a record — body {"text": "..."} |
| DELETE | /api/items/:id | Deletes a record |
curl -s BASE/api/status | jq
curl -s -X POST BASE/api/items -H 'content-type: application/json' -d '{"text":"hello from curl"}'
curl -s BASE/api/items | jq
curl -s -X DELETE BASE/api/items/<id> -i
npm test runs tests/app.test.js with Node's native runner
(node --test, zero dependencies): it boots the real app on an ephemeral port and
walks the whole API in memory mode — health, status, the full CRUD, 400 validation and 404,
and that the frontend is served. GitHub Actions (ci.yml) runs it on every push and PR.
flowchart LR
T[node --test] --> L[app.listen ephemeral port]
L --> H[health + status]
L --> C[POST → GET → DELETE items]
L --> E[errors 400 / 404]
L --> F[static frontend]