Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 104 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
<div align="center">

# StockSync

**Inventory that never loses a sale — even offline.**

An offline-first inventory sync engine for kirana shops running more than
one billing counter on a single, often-unreliable internet connection.

[![CI](https://github.com/Monolithic-Dev/stocksync/actions/workflows/ci.yml/badge.svg)](https://github.com/Monolithic-Dev/stocksync/actions)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)
![AWS CDK](https://img.shields.io/badge/AWS%20CDK-FF9900?logo=amazonaws&logoColor=white)
![React](https://img.shields.io/badge/React-61DAFB?logo=react&logoColor=black)
![DynamoDB](https://img.shields.io/badge/DynamoDB-4053D6?logo=amazondynamodb&logoColor=white)

[**Live demo**](https://main.d18ash44o1uc8d.amplifyapp.com) · [Demo video](#) · [Architecture](#architecture) · [Docs](docs/general/00-INDEX.md)

</div>

---

Two counters can sell the same item while both are offline, for any
duration, in any order — and when they reconnect, StockSync guarantees the
stock count reconciles to one mathematically correct, non-lossy state,
Expand All @@ -10,11 +28,24 @@ with a minimal client, **StockSync Counter**, that exists to make the
engine's correctness demonstrable on screen, not to be a sellable
point-of-sale product.

- **Live demo:** https://main.d18ash44o1uc8d.amplifyapp.com
(sign up to create a shop, or sign in to an existing one — shop identity now comes from your
Cognito account, not a URL query param; open a second window signed into the same shop to see
the two-counter conflict scenario)
- **Demo video:** _TODO — add the recorded demo video link here._
> **Live demo:** https://main.d18ash44o1uc8d.amplifyapp.com
> — sign up to create a shop, or sign in to an existing one (shop identity
> comes from your Cognito account, not a URL query param); open a second
> window signed into the same shop to see the two-counter conflict scenario.
>
> **Demo video:** _TODO — add the recorded demo video link here._

## Contents

- [The problem, in one paragraph](#the-problem-in-one-paragraph)
- [Architecture](#architecture)
- [What makes this correct, not just working](#what-makes-this-correct-not-just-working)
- [Running it locally](#running-it-locally)
- [What we learned](#what-we-learned)
- [Project structure](#project-structure)
- [Scope](#scope)
- [Beyond the core sync engine — also shipped](#beyond-the-core-sync-engine--also-shipped)
- [Where this goes next](#where-this-goes-next)

## The problem, in one paragraph

Expand All @@ -31,23 +62,76 @@ concurrent changes to the same shared number correctly, which is exactly
what a CRDT is built for. See [`docs/general/01-PRD.md`](docs/general/01-PRD.md)
§2 for the full problem statement.

## Architecture, summarized
## Architecture

```mermaid
flowchart TB
subgraph Client["StockSync Counter — React"]
UI["Offline queue (IndexedDB)"]
end

subgraph Gateway["Amazon API Gateway"]
REST["REST API<br/>Cognito JWT authorizer"]
WS["WebSocket API<br/>$connect / $disconnect"]
end

subgraph Compute["AWS Lambda"]
Intake["write-intake<br/>idempotency check"]
Resolver["conflict-resolver<br/>packages/core merge logic"]
Push["ws-push"]
Notify["notify-alerts"]
Auth["Cognito triggers<br/>postConfirmation · staffInvite"]
end

Queue["Amazon SQS FIFO<br/>MessageGroupId = item_id"]

subgraph Data["Amazon DynamoDB"]
Records[("inventory_records")]
Dedup[("write_dedup")]
Audit[("audit_log")]
Conns[("ws_connections")]
end

Streams["DynamoDB Streams"]
Bedrock["Amazon Bedrock<br/>conflict explanations · advisory only"]
SES["Amazon SES<br/>low-stock / conflict emails"]
Cognito["Amazon Cognito<br/>owner · manager · counter_staff"]
CW["Amazon CloudWatch<br/>ConflictRate · IdempotencyHitRate"]

UI -- "POST /transactions" --> REST
REST --> Intake
Intake -- "enqueue, ordered per item" --> Queue
Queue --> Resolver
Resolver -- "TransactWriteItems" --> Records
Resolver --> Dedup
Resolver --> Audit
Resolver -. "same-field conflict" .-> Bedrock
Records --> Streams
Streams --> Push
Streams --> Notify
Push -- "WSS live push" --> WS
WS --> UI
Notify --> SES
Cognito --> Auth
Auth -. "owner group on signup" .-> Records
REST -. verifies JWT .-> Cognito
Compute -. "ConflictRate / IdempotencyHitRate" .-> CW

classDef aws fill:#FF9900,stroke:#232F3E,color:#232F3E,font-weight:bold;
classDef data fill:#4053D6,stroke:#232F3E,color:#fff,font-weight:bold;
classDef client fill:#61DAFB,stroke:#232F3E,color:#232F3E,font-weight:bold;
class REST,WS,Queue,Bedrock,SES,Cognito,CW aws;
class Records,Dedup,Audit,Conns data;
class UI client;
```
StockSync Counter (React, offline queue via IndexedDB)
│ POST /transactions ▲ WSS live push
▼ │
API Gateway (REST + WebSocket)
│
▼
Lambda: write-intake → SQS FIFO (MessageGroupId = item_id) → Lambda: conflict-resolver
│ idempotency check via write_dedup │ imports packages/core for all merge logic
│ │ TransactWriteItems: inventory_records + audit_log + write_dedup
│ │ Bedrock for same-field conflict explanations (non-blocking)
▼ ▼
Amazon DynamoDB
inventory_records / write_dedup / audit_log / ws_connections
```

The detail that matters most and is easiest to get wrong: **`MessageGroupId`
is the inventory item's ID, not the sending counter's ID.** Group by
counter instead, and two counters editing the same item race each other in
parallel Lambda invocations — which reintroduces the exact bug this
project exists to prevent.

## What makes this correct, not just working

Every one of these pieces is load-bearing, not decorative:

Expand Down
229 changes: 229 additions & 0 deletions docs/SUBMISSION-FORM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
# StockSync — Hackathon Submission Form (draft answers)

Draft, ready-to-paste answers for the AWS First Commit hackathon submission
form. Review each field, edit anything that doesn't sound like you, fill in
the two fields only you can (video link, team contributions), then paste
into the actual form.

---

## YouTube video demo link *

_TODO — record and paste the link here. Must be published or unlisted, ≤3
minutes, covering: about the project, tech stack and architecture, how you
used AWS, and learning/growth (optional). See
[`docs/general/10-DEMO-PLAN.md`](general/10-DEMO-PLAN.md) for the full
rehearsed script — it's built to hit all four points inside 3 minutes._

---

## What does your project do? *

> What problem does your project solve, and who is it for?

StockSync is an offline-first inventory sync engine for small Indian shops
("kirana stores") that run more than one billing counter on a single,
often-unreliable internet connection — a second register during rush hour,
or an owner checking stock from their own phone.

When the connection drops and two counters sell the same item while both
are offline, most systems either block the sale or let whichever counter
reconnects last silently overwrite the other's numbers — erasing a real
sale from the record. Nobody notices until the stock count is wrong and a
customer is told an item is in stock when it isn't. For thin-margin retail,
that's a recurring, real cost, not an edge case.

StockSync makes that mathematically impossible. Two counters can go
offline, sell the same item, restock it, and change its price — in any
order, for any duration — and when they reconnect, the stock count always
converges to one correct, non-lossy state. Genuine same-field conflicts
(e.g. two different prices set concurrently) are never silently guessed;
they're flagged for a human, with an AI-generated plain-language
explanation to help them decide fast.

It's built for small multi-counter retailers in India today, but the
underlying problem — two offline devices sharing one number that must
reconcile correctly — applies to any offline-tolerant system, not just
point-of-sale.

---

## How did you use AWS in your project? *

> Build it: How have you used AWS open source stack (mention tools name)
> Ship it: How have you used AWS services (mention services name)

**Build it — AWS open-source tooling:**
- **AWS CDK** (TypeScript) — the entire stack (DynamoDB tables, Lambdas,
API Gateway REST + WebSocket, Cognito, SQS, SES, CloudWatch) is defined
as code in `infra/cdk`, with unit tests asserting IAM policies never use
a wildcard resource (with one documented, unavoidable exception —
`dynamodb:ListStreams`, which AWS has no resource-level IAM scoping for
at all, confirmed by inspecting the synthesized CloudFormation template).
- **AWS SDK for JavaScript v3** (`@aws-sdk/*`) — every Lambda handler talks
to DynamoDB, SQS, SES, Cognito, Secrets Manager, and Bedrock through the
v3 modular clients.
- **aws-sdk-client-mock** — every AWS SDK call in the Lambda test suite is
mocked at the client level, not the network level, keeping ~150+ tests
fast and deterministic.

**Ship it — AWS services in production:**
- **AWS Lambda** — every unit of compute: write intake, conflict
resolution, sync/audit queries, WebSocket connect/disconnect/push,
Cognito triggers (post-confirmation, staff invite), CRUD/checkout
handlers, and the SES notification consumer.
- **Amazon DynamoDB** — the system of record (`inventory_records`,
`write_dedup`, `audit_log`, `ws_connections`, plus the Tier-2 tables:
`products`, `categories`, `suppliers`, `orders`), with DynamoDB Streams
driving both real-time WebSocket push and the SES alert pipeline.
- **Amazon SQS (FIFO)** — orders every write touching a given inventory
item strictly by item ID (not by sending counter), which is the single
most load-bearing correctness guarantee in the whole system.
- **Amazon API Gateway** — both REST (write/sync/CRUD/checkout endpoints)
and WebSocket (live push to open counters), with a Cognito JWT authorizer
on every REST route.
- **Amazon Cognito** — real multi-tenant authentication: self-signup
creates a shop and its owner; owners invite manager/counter-staff
accounts; every request's `shop_id` comes from the verified JWT, never
a client-supplied value.
- **Amazon Bedrock** (Claude Haiku) — generates a plain-language
explanation of a genuine same-field conflict for the human resolving it
— strictly advisory, never the thing that decides the value.
- **Amazon SES** — low-stock and conflict-review email alerts to a shop's
owner, triggered off the same DynamoDB Stream that already powers
real-time sync.
- **Amazon CloudWatch** — custom `ConflictRate`/`IdempotencyHitRate`
metrics plus queue-depth dashboards.
- **AWS Secrets Manager** — holds the cross-account Bedrock credentials
(see the AWS feedback section below for why that workaround exists).
- **AWS Amplify Hosting** — serves the React client.

---

## Blog links

_Publish `docs/BLOG-POST.md` to the AWS Builder Center, then paste the
published link here. The draft is already written — "Two billing
counters, one bad connection, and the bug that erases a sale" — covering
the PN-Counter/vector-clock design decisions, the load-bearing
`MessageGroupId = item_id` detail, and a real concurrency bug an edge-case
hardening pass caught. Nothing left to do but publish it and paste the
link._

---

## Team leader's contributions *

> Individual roles and key deliverables completed by team member.

_TODO — edit this to describe your actual role. Draft below assumes a
solo build; adjust if that's not accurate:_

**Maha Kisore** — sole builder. Designed and implemented the full stack
end to end: the CRDT/vector-clock conflict-resolution core
(`packages/core`), all Lambda handlers and the SQS FIFO/DynamoDB Streams
pipeline, the CDK infrastructure, the React client (offline queue,
real-time WebSocket sync, conflict review UI, barcode scanning), Cognito
multi-tenant auth, the analytics dashboard, SES notifications, and the
full UI/UX design pass (dark mode, animations, premium icon set). Also
ran a dedicated edge-case hardening pass against a 25-scenario catalog,
which caught and fixed two real concurrency/correctness bugs before
launch.

## Second team member's contributions

_Leave blank if solo, or fill in if you had a teammate._

## Third team member's contributions

_Leave blank if solo, or fill in if you had a teammate._

---

## Help us evaluate you: your feedback on the AWS services you used *

> Tell us what you didn't like about the AWS services you used and what
> could be better, whether technical or onboarding related.

**Bedrock's account-level model-access gating was the single biggest
friction point.** A brand-new AWS account can't invoke Bedrock models at
all until an "Anthropic use-case details" approval clears — and that
approval goes through a Support case with no guaranteed SLA on Basic
support, which is exactly the plan every new/personal AWS account starts
on. For a hackathon with a hard deadline, an unpredictable, un-appealable
wait on a **generative AI feature specifically** is a real barrier —
Bedrock is the one service most likely to be the differentiator in a
project like this, and it's also the one most likely to be blocked when
you need it most. A self-service "instant sandbox" tier for Bedrock,
similar to how most other services just work the moment you enable them,
would remove a lot of unnecessary hackathon friction.

**AWS Marketplace's payment-instrument checks were opaque.** Separately,
provisioning a Bedrock model subscription through a second account failed
with `INVALID_PAYMENT_INSTRUMENT` even with a valid card on file as the
account default — it turned out the account's *default payment method*
was set to UPI AutoPay, which Marketplace subscriptions silently reject in
favor of a credit/debit card, with no explicit error message pointing at
that as the cause. Discovering the actual root cause took directly testing
the API call and reading the raw exception text, not anything AWS's UI
surfaced proactively.

**SES sandbox mode's requirement that sender *and every recipient* be
individually verified** makes sense for abuse prevention, but for a
demo/hackathon context it means the "real" version of a notification
feature (alerting an arbitrary shop owner's real email) isn't actually
testable without a production-access request — so we could only verify
the mechanism works, not deliver a fully realistic demo of it.

**Amplify Hosting's CLI deploy flow (no GitHub App connected) requires
manually scripting** `create-deployment` → zip → presigned-URL `curl` →
`start-deployment` → poll — it works, but it's clearly the secondary path
next to the git-connected auto-deploy flow, and a couple of the CLI
commands (e.g. `create-deployment` rejecting a second call while a prior
job is still `PENDING`) produce cryptic-enough errors that recovering
requires cross-referencing `get-job` state manually rather than anything
self-explanatory.

---

## What did you like about the AWS services you used? *

> Tell us what worked well for you, whether that was a specific service,
> the docs, the setup, or anything that made building easier.

**CDK made "infrastructure as code" genuinely load-bearing, not just a
buzzword.** Being able to write a unit test that asserts *no IAM policy in
the synthesized template uses a wildcard resource* — and have it actually
catch a real over-broad grant during development — is a level of
confidence a console-clicked stack can't give you. The one time the test
needed an exception (`dynamodb:ListStreams`, which AWS itself has no
resource-level IAM scoping for), CDK's synthesized template made that
completely verifiable rather than a guess.

**DynamoDB Streams + `TransactWriteItems`** were a genuinely elegant fit
for this problem: Streams became a free, already-running event source we
reused for real-time WebSocket push *and* email alerts, without adding any
new write-path code, and `TransactWriteItems` meant a record update, its
audit-log entry, and its idempotency claim could never drift out of sync
with each other even under a crash mid-write.

**Cognito's built-in triggers (post-confirmation, custom user creation)**
made "self-signup creates a shop and makes you its owner" a clean,
declarative flow instead of a custom auth service — and the JWT authorizer
on API Gateway meant every route got tenant isolation for free, without
hand-rolling token verification in every handler.

**The AWS SDK v3's modular clients paired extremely well with
`aws-sdk-client-mock`** — every Lambda handler's AWS calls could be
unit-tested in milliseconds against a mocked client, while the
correctness-critical DynamoDB paths were additionally tested against a
real local DynamoDB instance (`dynamodb-local`) for true integration
coverage. That two-tier testing setup, entirely enabled by how the v3 SDK
is structured, is a big part of why this project has ~200+ tests across
the stack.

**Overall, the actual serverless services (Lambda, DynamoDB, SQS, API
Gateway, Cognito) just worked** — provisioned cleanly through CDK, no
console clicking required, and behaved exactly as documented. The friction
in this build was almost entirely concentrated in Bedrock/Marketplace
account-gating, not in the core services the architecture is built on.
Loading