Try it live: demo.pulse.kartikeybhardwaj.com · GitHub mirror
A minimalistic, open-source, real-time polling app built for small and large orgs that want quick decisions without the bloat.
Most polling tools are either buried inside heavy platforms (Slack, Teams, Google Forms), locked behind paywalls, or overloaded with features nobody asked for. Pulse is different — it's a standalone app that does one thing well: fast, neat, real-time polls.
Why Pulse?
pulse/
├── infra/ # AWS CDK (Python)
│ ├── stack.py # Main stack — composes all resources
│ └── resources/ # Modular CDK constructs
│ ├── database.py # DynamoDB table + GSIs
│ ├── rest_api.py # REST API Gateway + Lambdas + rate limits
│ ├── websocket_api.py # WebSocket API Gateway + Lambdas
│ └── frontend.py # S3 + CloudFront + OAC
├── backend/ # Lambda functions (Python 3.12)
│ ├── handlers/ # Thin Lambda entry points
│ │ ├── auth.py # Signup, verify, signin, forgot, reset
│ │ ├── rest_api.py # Poll CRUD, voting, listing
│ │ ├── ws_connect.py # WebSocket $connect
│ │ ├── ws_disconnect.py # WebSocket $disconnect
│ │ └── ws_subscribe.py # WebSocket subscribe
│ ├── lib/ # Business logic
│ │ ├── db/ # DynamoDB operations (users, polls, votes, connections)
│ │ ├── models/ # Dataclasses (User, Poll, Vote)
│ │ ├── auth_service.py # Password hashing, JWT tokens, SES email
│ │ ├── broadcast.py # WebSocket broadcast to subscribers
│ │ ├── polls.py # Poll response builder + anonymity enforcement
│ │ ├── response.py # HTTP response helper + Decimal JSON encoder
│ │ └── utils.py # Constants + helpers (gen_id, sanitize_url)
│ └── tests/ # pytest + moto (mocked DynamoDB/SES/SSM)
└── frontend/ # SvelteKit SPA (Svelte 5)
└── src/
├── lib/ # API client, WebSocket store, auth, theme, toasts
└── routes/ # Pages (recent, mine, new, auth, poll detail)
Browser → CloudFront → S3 (SPA static files)
→ API Gateway (REST) → Lambda → DynamoDB
→ WebSocket API Gateway → Lambda → DynamoDB
← Lambda (broadcast via API GW Management API)
/api/* to API GatewayAuthorization: Bearer <token> on all API calls/api//api/auth/*): signup, email verification, signin, password reset — handled by auth.py Lambda/api/polls/*): CRUD, voting, edit, close/reopen — handled by rest_api.py Lambda$connect, $disconnect, and subscribe routes, each backed by a separate LambdaSUB#<pollId>/CONN#<connId> in DynamoDBEMAIL#<email> lookup item in DynamoDBPOLL#, VOTE#, USER#, EMAIL#, CONN#, SUB#, CREATOR#POLLS partition — recent polls listing (sorted by creation time)CREATOR#<alias> partition — "My Polls" listing (no in-memory filtering)strip_poll_for_response() — the API never returns data the frontend shouldn't shownpm install -g aws-cdk)Configure your AWS credentials via your preferred method (aws configure, environment variables, or SSO).
aws sts get-caller-identity
You should see your account ID in the output.
cd frontend
npm install
npm run build
cd ..
This outputs static files to frontend/build/ which CDK will deploy to S3.
cd infra
python3 -m venv .venv && source .venv/bin/activate
pip install .
Bootstrap CDK (first time only):
cdk bootstrap aws://<YOUR-AWS-ACCOUNT-ID>/us-east-1
Create JWT secret (first time only):
aws ssm put-parameter --name /pulse/jwt-secret --value $(openssl rand -hex 32) --type String --region us-east-1
Deploy:
[email protected] cdk deploy --require-approval never
CDK will output:
| Output | Description |
|---|---|
RestApiUrl |
REST API endpoint |
WsApiUrl |
WebSocket API endpoint |
DistributionUrl |
CloudFront URL (your app) |
TableName |
DynamoDB table name |
After the first deploy, grab the WsApiUrl from the CDK output and rebuild the frontend with it:
cd frontend
VITE_WS_URL=wss://<ws-api-id>.execute-api.us-east-1.amazonaws.com/prod npm run build
cd ../infra
[email protected] cdk deploy --require-approval never
This second deploy pushes the updated frontend build with the WebSocket URL baked in.
Open the DistributionUrl in your browser. You should see the Pulse app. CloudFront may take a few minutes to propagate.
Pulse includes a fully functional demo mode that runs entirely in the browser — no backend or AWS account needed. All data is stored in localStorage with pre-seeded polls and users.
To run the demo locally:
cd frontend
npm install
VITE_DEMO=true npm run build
npm run preview
Pre-seeded accounts:
| Password | Role | |
|---|---|---|
[email protected] |
demo |
Owns 3 polls (techstack, spirit, private) |
[email protected] |
alice |
Has votes on multiple polls |
[email protected] |
bob |
Owns the lunch poll |
Or sign up with any new credentials. Clear localStorage to reset the demo to its seeded state.
cd frontend
npm install
npm run dev
The Vite dev server proxies /api/* requests to http://localhost:3001 (configure in vite.config.js if your local backend runs elsewhere).
Note: WebSocket real-time updates are not available in local dev (no local WS server). The app works normally — votes just won't push live to other tabs. To test WebSocket, deploy to AWS and set
VITE_WS_URL.
cd infra && source .venv/bin/activate
pip install -e "../backend[dev]"
cd ../backend && python -m pytest tests/ -v
# Backend (Python)
cd backend && ruff format . && ruff check .
# Frontend (Svelte/JS)
cd frontend && npx prettier --write "src/**/*.{js,svelte}"
# If frontend changed
cd frontend && VITE_WS_URL=wss://<ws-api-id>.execute-api.us-east-1.amazonaws.com/prod npm run build && cd ..
# Deploy
cd infra && source .venv/bin/activate && [email protected] cdk deploy --require-approval never
cd infra
source .venv/bin/activate
cdk destroy
This removes all resources (DynamoDB table, Lambdas, API Gateways, S3 bucket, CloudFront distribution).
| Variable | Where | Description |
|---|---|---|
VITE_WS_URL |
Frontend build | WebSocket API URL (from CDK output) |
SES_FROM_EMAIL |
CDK deploy | SES verified sender email |
TABLE_NAME |
Lambda (auto) | DynamoDB table name (set by CDK) |
WS_API_ENDPOINT |
Lambda (auto) | WebSocket management URL (set by CDK) |
JWT_SECRET_PARAM |
Lambda (auto) | SSM parameter name for JWT secret |
All limits are per-second (rate) with burst capacity. Enforced at API Gateway level — returns 429 Too Many Requests when exceeded.
| Endpoint | Rate (req/s) | Burst |
|---|---|---|
POST /auth/signup |
5 | 10 |
POST /auth/signin |
10 | 20 |
POST /auth/verify |
5 | 10 |
POST /auth/resend |
2 | 5 |
POST /auth/forgot |
2 | 5 |
POST /auth/reset |
5 | 10 |
GET /auth/me |
50 | 100 |
GET /polls |
30 | 60 |
POST /polls |
10 | 20 |
GET /polls/:id |
30 | 60 |
POST /polls/:id/vote |
20 | 40 |
PUT /polls/:id (edit) |
5 | 10 |
PATCH /polls/:id (close/reopen) |
5 | 10 |
DELETE /polls/:id |
5 | 10 |
| Global default | 50 | 100 |
Based on AWS pricing (March 2026) for us-east-1. Most services fall within the AWS Free Tier for low-to-moderate usage.
These limits reset monthly and never expire. "Overage Price" applies only if you exceed these limits.
| Service | Pulse Usage | Free Tier Limit | Overage Price (if exceeded) |
|---|---|---|---|
| Lambda | 6 functions | 1M requests + 400k GB-s | $0.20 / 1M requests + $0.0000167/GB-s |
| CloudFront | CDN Proxy | 1 TB Data Transfer Out | $0.085 / GB (US/Europe) |
| SSM Params | JWT Secret | 10,000 standard params | $0.05 / param (Must upgrade to "Advanced") |
| DynamoDB | Database | 25 GB Storage | $0.25 / GB |
These expire 1 year after sign-up. Standard rates apply afterwards.
| Service | Pulse Usage | Free Tier (Monthly) | Standard Price (Post-Trial) |
|---|---|---|---|
| API Gateway | REST API | 1M calls | $3.50 / million calls |
| API Gateway | WebSocket | 1M msgs + 750k conn-min | $1.00 / million msgs |
| S3 | Static Assets | 5 GB Storage + 20k GETs | $0.023 / GB |
| SES | Emails | 3,000 emails | $0.10 / 1,000 emails |
| Usage Level | Estimated Cost | Notes |
|---|---|---|
| Light (< 100 users) | $0.00 | Fully covered by Free Tier (using Provisioned DynamoDB) |
| Medium (~50k votes) | ~$1.25 | Primary cost is DynamoDB Write Units |
| Heavy (~500k votes) | ~$18.50 | Driven by API Gateway & Database throughput |
All pricing is for us-east-1. Use the AWS Pricing Calculator for precise estimates.