🧩 vedtemplate · Vercel + Express + DynamoDB

Self-explanatory showcase: what the template contains, how everything is connected, how it is deployed, plus a working demo against the database.

loading…

Overview

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

Request flow (adding a record)

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
      

Data model: single-table design

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.

pkskattributespurpose
ITEM<isoDate>#<uuid>id, text, createdAtDemo records (chronological order for free via Query)
YOUR_ENTITYwhatever you needAdd the prefix in src/config/dynamo.js and create a service

Repository structure

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]
      

Design decisions

📡 Web traffic intelligence

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.

🔒 Privacy by design. This page is public, so sensitive request/response values (Authorization headers, cookies, query strings) are captured for auditing but never stored or shown in clear — you only see that they were captured. IP addresses are masked (last octet dropped) and additionally salted-hashed to count unique visitors without revealing anyone's address. Do-Not-Track is honoured.

Accesses over the last 24h (humans vs bots)

🌍 Top countries

📄 Top pages

🔗 Referrers

🌐 Browsers

💻 OS

📱 Devices

🛰️ Live access feed (redacted)

🧑‍🤝‍🧑 Visitor explorer

Each visitor is a browser fingerprint (canvas + non-PII signals, hashed). It lets us count unique people and spot returning ones without a login.

How it works

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

Live connection status

Data served by GET /api/status: the backend checks its environment variables and performs a real DescribeTable against DynamoDB.

loading…

Raw response

Demo: records against the database

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.

loading…

Tip: open this page in two tabs and add a record in one; refresh the other to see the shared persistence.

Deployment pipeline

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

Steps, in order

  1. Try it locally (0 external dependencies)
    npm install
    npm test        # 6 tests, memory mode
    npm run dev     # http://localhost:3000
  2. Push to GitHub — create the repo manually if it does not exist (github.com/new or 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.

  3. Deploy to Vercel (still without AWS) — import the repo at vercel.com/new. It detects vercel.json and publishes. The app already works, in memory mode.
  4. Create the AWS infrastructure — two options:
    # 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
  5. Create the app's IAM user and connect Vercel
    ./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.
  6. Seed sample data (optional)
    cp .env.example .env   # paste the same credentials there
    npm run seed

100% CLI deployment — the actual flow used for this instance

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.

Environment variables

VariableWherePurpose
AWS_REGIONVercel + .envTable region (eu-west-1)
DYNAMODB_TABLEVercel + .envTable name
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYVercel + .envIAM user scoped to the table (setup-aws.sh)
CORS_ORIGINSoptionalOnly if another site calls this API
AWS_ACCESS_KEY_ID/SECRET (GitHub secrets)repoPipeline deploy user (setup-github-secrets.sh)

🔎 SEO — how this site gets found on Google

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.

How a search engine works, in 3 steps

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]

What this template already does

What only you can do (off-page)

💡 In short: this page is already built to be found when someone searches for automated infrastructure, Vercel/DynamoDB templates, or web-traffic tooling — and it credits Infranettone and its founder in machine-readable form. The rest is links and time.

Endpoints

MethodPathDescription
GET/api/status/healthInstant health check (for monitors)
GET/api/statusFull status: env, runtime, DynamoDB with latency
GET/api/itemsLists records (max 100, newest first)
POST/api/itemsCreates a record — body {"text": "..."}
DELETE/api/items/:idDeletes a record

Try it with curl

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

Automated tests

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]