Engineering reference: the visitor check-in logger architecture
The first six posts are for the person deciding whether to build this. This one is for the person building it. Same system, no analogies: the services by name, the functions, the two tables, the offline roll call, and the retention rule.
Key takeaways
- Single region, single account. Every resource is regional; nothing is global except the IAM roles.
- 3 Lambda functions, each with its own execution role. No shared role, no wildcards on resources.
- 2 DynamoDB tables, each keyed so the concurrency story is a condition expression rather than a lock.
- One Bedrock model, called once, with a JSON schema it must fill or leave null.
- Nothing always-on: no instance, no container, no provisioned capacity.
The system, by service name
- Compute
- Database
- App integration
- Networking
- Security & identity
- Front-end & mobile
Region and account
- Region:
us-east-1. Chosen because SES inbound receipt rules exist in only a subset of regions and this one has the widest Bedrock model availability. If your data has to stay elsewhere, check both constraints before moving: inbound SES is the binding one. - Account: one. This is a small system, and a separate account per environment costs more in wiring than it saves. A
devand aprodstack in the same account, with distinct resource prefixes, is the right size here. - Everything is regional. The only global resources are the IAM roles and policies. There is no CloudFront, no global table and no cross-region replication, because nothing here has a latency or durability requirement that would justify them.
Lambda inventory
| Function | Trigger | Does | Timeout / memory |
|---|---|---|---|
vl-checkin | API, from the screen | Creates the visit, notifies the host, checks induction currency for known contractors | 10s / 512 MB |
vl-checkout | API, from any of the four mechanisms | Closes the visit as confirmed, recording which mechanism did it | 10s / 512 MB |
vl-sweep | EventBridge, at the site’s stated end of day | Sends a final host prompt, then closes the rest as assumed; never runs mid-day | 60s / 512 MB |
Splitting this into separate functions is not about modularity. It is that only one of them needs Bedrock permissions and only one is reachable from the public internet, and neither of those is true if it is one handler behind a router.
IAM, scoped
| Role | Allowed | On |
|---|---|---|
vl-checkin-role | dynamodb:PutItem, dynamodb:GetItem, ses:SendEmail | Visits; read-only on people; one verified identity |
vl-checkout-role | dynamodb:UpdateItem | Visits only |
vl-sweep-role | dynamodb:Query, dynamodb:UpdateItem, ses:SendEmail | Visits; one verified identity |
No role has a Resource: “*” on anything that writes, and every GetSecretValue grant names a single secret arn. That is why there is more than one secret rather than one JSON blob with everything in it.
DynamoDB schemas
Table: visits
PK site_id S
SK checked_in_at#id S sorts the roll call by arrival naturally
name S
host_id S the roll call groups on this
kind S visitor | contractor | delivery
checked_out_at S
checkout_by S visitor | host | badge | sweep
confirmed BOOL false when closed by the sweep
mobile S only if the site uses it, and says what for
ttl N epoch, +30 days
`checkout_by` is what makes the confirmed-versus-assumed chart in
Part 4 possible, and that chart is the only measure of accuracy.
Table: people
PK person_key S a code on a contractor card
display_name S
kind S contractor | regular_visitor
inducted_at S
induction_expires S checked at every check-in
inducted_by S a person, recorded
rules_version S which site rules the induction covered
Deliberately holds no certificate scans. Recording that competencies
were checked, by whom and when, is the assurance; the documents are
somebody else’s personal data.
Inbound and outbound
- Check-in is name and host and nothing else unless the site has stated a reason for another field.
- The roll call is pushed to devices and cached, refreshed while a network exists and readable when it does not. It requires no login at the point of use.
- Four check-out paths write the same record with different
checkout_byvalues, so coverage per mechanism is measurable. - The sweep time is per site, with a separate out-of-hours configuration, because a nine-to-five sweep would close a Sunday shift.
The model call
- There is no model in this system. It is a list with timestamps.
- The tempting use is predicting when a visitor has probably left. That is exactly what the assumed sweep does, and dressing it up as a prediction would let it run mid-day.
- The wrong use is facial recognition. It converts a visitor log into a biometric system with a different set of obligations and no additional safety benefit.
- A defensible use is matching a typed name against known contractors, so a regular does not create a new person record every visit.
- The cost page assumes none, which is why messaging is the whole variable.
Things worth knowing before you build it
- Never run the sweep during the working day. Auto-closing somebody at two o’clock removes them from the roll call at exactly the hour the alarm might go.
- Record which mechanism closed each visit. Without it, confirmed and assumed are indistinguishable and the roll call’s accuracy cannot be measured or improved.
- Group the roll call by host, not alphabetically. It matches how confirmation actually happens at an assembly point.
- Cache the roll call on the devices. A web page is not available in a car park with the power off.
- Set a TTL on visit records from the first day. A multi-year log of who met whom is a liability nobody chose to create.
That is the whole system. Seven posts, one diagram at a time, and nothing in it that needs a server.
All posts