AOVIS Cloud Storage Phase Handoff — 2026-05-13
This document records the production-facing cloud storage work completed on 2026-05-13 for aovis.app / aovis-direct-store.
Scope
Implemented and verified the account-side cloud storage subscription foundation for NEXA devices:
- Device activation cloud trial provisioning.
- Cloud storage plan definitions and entitlement-based access windows.
- AWS Kinesis Video Streams physical retention set to 30 days.
- Stripe test-mode cloud storage subscription checkout.
- Stripe webhook to local
Subscription/Entitlementlifecycle. - Frontend service-page buttons connected to cloud storage checkout.
- Stripe Billing Portal entry from Account Services.
This does not mean real camera video upload/playback is complete. KVS access can reach AWS, but without real device fragments AWS returns expected video-layer errors.
Test user and first dry-run device
- User email:
[email protected] - User id:
cmne10rir000h8mwi8gvxvwqs - Device AOVIS id:
aovis-n4k-000001 - Device id:
cmp3hyo970000qm97c1foeybk - Serial:
N4K-000001 - Model:
NEXA Prime 4K - ICCID:
8910300000050270479 - AWS IoT Thing:
aovis-dev-aovis-n4k-000001 - KVS Stream:
aovis-stream-aovis-n4k-000001 - WebRTC Channel ARN:
arn:aws:kinesisvideo:us-east-1:288669178338:channel/aovis-webrtc-aovis-n4k-000001/1778642368962
Release lineage
Relevant releases and operations:
d0634f8— device activation cloud trial provisioning and non-expired entitlement check.7941130— cloud storage plan definitions and app-layer access window checks.703edeb— KVS retention tooling and default 720h configuration.- AWS IAM policy update —
aovis-backend-service-policynew default versionv2includeskinesisvideo:UpdateDataRetention. - Production VM
.env.productionupdated:KVS_DATA_RETENTION_HOURS=720and PM2 restarted with--update-env. - All current DB devices with KVS streams applied to 720h retention; verified first stream shows
Current retention: 720h. 03bee12— cloud storage subscription checkout/webhook entitlement issuance.d692790— Stripe subscription period extraction fixed with item-level fallback.92795fc— frontend service-page subscription buttons connected to cloud storage checkout.be17b03— fixed frontend device id bug (DeviceOwnership.idwas incorrectly submitted instead ofDevice.id).fe3e255— Stripe Billing Portal route and Account ServicesManage billingentry.
Cloud trial policy
Business rule:
- Every device gets one 30-day Daily Backup Starter Trial on first binding activation.
- Trial entitlement:
cloud_playbackquota1(24-hour rolling history). - Same
userId + deviceIdrepeated activation must be idempotent and must not extend trial. - Future device transfer / rebind must not grant a second free device trial; trial is device-level and first-activation-only.
Implementation:
lib/cloud-trial.tsapp/api/devices/activate/route.ts- Trial product sku:
aovis-daily-backup-starter-trial - Subscription external id pattern:
device-trial:${deviceId}:${userId}
Paid cloud storage plans
Plan definitions are centralized in lib/cloud-storage-plans.ts.
| Plan | Price | Retention / quota | Entitlements |
|---|---|---|---|
| Local | $0 | 0 | no cloud checkout |
| Starter | $1.99/month | 1 day | cloud_playback, ai_summary, smart_search |
| Standard | $4.99/month | 7 days | cloud_playback, ai_summary, smart_search |
| Premium | $9.99/month | 30 days | cloud_playback, ai_summary, smart_search |
Important product decision:
- Starter / Standard / Premium have the same core cloud service capabilities.
- Main difference is cloud history retention window (
cloud_playbackquota), not AI feature gating.
Entitlement access window
lib/cloud-access-window.tsvalidates requested history range againstcloud_playbackquota./api/devices/[id]/clipand on-demand/api/devices/[id]/streamenforce the window.- Live stream path is not gated by cloud retention history.
lib/aws/device-service-access.tsselects the highest active quota (orderBy: { quota: "desc" }), so a paid Standard quota=7 overrides trial quota=1.
Expected behavior:
- Trial-only user requesting 2-day-old clip should receive
403 time_range_exceeds_entitlement. - Paid Standard user requesting 2-day-old clip should pass app-layer entitlement check and reach AWS KVS.
- Without real fragments, AWS KVS can return
502 clip_error/ retention or no-fragment errors; that is video-layer behavior, not subscription failure.
AWS KVS retention
- Target physical KVS retention: 720 hours / 30 days.
- Application layer controls user access at 1 / 7 / 30 days.
- This avoids per-device AWS retention churn while preserving product tier enforcement in app code.
Files:
lib/cloud-storage-retention.tslib/aws/kvs-retention.tsscripts/kvs-retention-reconcile.tsdocs/cloud-storage-retention.md
Production state:
KVS_DATA_RETENTION_HOURS=720in/opt/aovis/aovis-store-staging/.env.production.aovis-stream-aovis-n4k-000001verified at720h.- IAM user
aovis-backend-servicehaskinesisvideo:UpdateDataRetentionthrough managed policyaovis-backend-service-policydefault versionv2.
Useful commands on production VM:
cd /opt/aovis/aovis-store-staging
set -a
. <(grep -E '^(AWS_REGION|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|DATABASE_URL)=' .env.production)
set +a
npm run kvs:retention:check -- --all-devices
npm run kvs:retention:apply -- --all-devices
Stripe cloud subscription checkout
Route:
POST /api/checkout/cloud-storage
Input form fields:
deviceId— must beDevice.id, notDeviceOwnership.id.planCode—starter,standard, orpremium.
Rules:
- User must be logged in.
- User must be
OWNERfor the device. - Route creates Stripe Checkout Session with
mode: "subscription". - Metadata includes
checkoutType=cloud_storage_subscription,userId,deviceId,planCode.
Stripe test-mode Price IDs configured in production .env.production:
STRIPE_CLOUD_STORAGE_STARTER_PRICE_ID=<set>STRIPE_CLOUD_STORAGE_STANDARD_PRICE_ID=<set>STRIPE_CLOUD_STORAGE_PREMIUM_PRICE_ID=<set>
The actual values must not be committed.
Stripe webhook and entitlement issuance
Webhook file:
app/api/stripe/webhook/route.ts
Cloud subscription events:
checkout.session.completedcheckout.session.async_payment_succeededcustomer.subscription.updatedcustomer.subscription.deleted
Key helpers:
mapStripeSubscriptionStatus()active/trialing->activecanceled/incomplete_expired->canceledpast_due,unpaid,incomplete, unknown ->inactive
extractStripeSubscriptionPeriod()- Uses top-level
current_period_start/endwhen present. - Falls back to
items.data[0].current_period_start/end. - Active/trialing without period throws; do not write
nowas an active expiry. - Canceled/inactive without period can use
nowfor safe expiration.
- Uses top-level
ensureCloudSubscriptionEntitlements()inlib/cloud-subscription-entitlements.ts- Upserts Product / Subscription / Entitlements.
- Active -> entitlement
ACTIVE. - Canceled/inactive -> Subscription
CANCELLED, entitlementsEXPIRED. - Idempotent for repeated Stripe events.
Verified paid Standard DB state after dry-run:
- Product:
aovis-cloud-storage-standard - Subscription: source
stripe, statusACTIVE, expires at2026-06-13 14:42:35. - Entitlements:
cloud_playbackquota=7 ACTIVE, expires2026-06-13 14:42:35ai_summaryACTIVE, expires2026-06-13 14:42:35smart_searchACTIVE, expires2026-06-13 14:42:35
- Trial quota=1 remains as history and fallback.
Frontend service pages
Files:
components/cloud-storage-subscribe-form.tsxapp/services/cloud-storage/page.tsxapp/services/ai-intelligence/page.tsx
Behavior:
- Local / Free plan shows Active and does not submit checkout.
- Starter / Standard / Premium submit to
/api/checkout/cloud-storage. - Unauthenticated user gets link to signin.
- Logged-in user with no owned devices links to
/account/devices. - One owned device posts hidden
deviceIdandplanCode. - Multiple owned devices render
<select name="deviceId">. - Important bug fixed in
be17b03: submitteddeviceIdmust beownedDevices[n].device.id, notownedDevices[n].id.
Billing Portal
Files:
app/api/billing/portal/route.tsapp/account/services/page.tsx
Behavior:
POST /api/billing/portalcreates Stripe Billing Portal session.- Only logged-in users can access.
- Optional local
subscriptionIdmust belong to current user. - Only
sourceChannel=stripeand product sku in cloud storage set are allowed. - Route retrieves Stripe subscription, gets customer id, creates portal session, and redirects to Stripe.
- No self-built cancel/update/refund logic exists.
Stripe test-mode portal configuration:
- Configuration ID:
bpc_1TWecuRdkjUYvVF62pZcenWR - Cancel mode: at period end.
- Payment method update: enabled.
- Invoice history: enabled.
- Cancel reason: enabled.
Known remaining work
- Real video cloud storage pipeline:
- Device/producer push to KVS.
- Real clip/history listing.
- Playback UI.
- AI summary and smart search implementation:
- Event indexing.
- Bedrock/OpenSearch/S3 integration as needed.
- Subscription lifecycle UI polish:
- Show active plan clearly in Account Services.
- Prevent duplicate purchase UX or guide to Billing Portal.
- Explicit cancel-at-period-end messaging.
- Device transfer / unbind / rebind:
- Must preserve no-second-free-trial policy.
- Must define how paid device-scoped subscriptions behave when ownership changes.
- Live Stripe migration later:
- Must switch all Stripe keys, webhook secrets, hardware prices, data-plan prices, and cloud storage prices together.
- Do not mix test and live modes.