Skip to main content

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 / Entitlement lifecycle.
  • 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-policy new default version v2 includes kinesisvideo:UpdateDataRetention.
  • Production VM .env.production updated: KVS_DATA_RETENTION_HOURS=720 and 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.id was incorrectly submitted instead of Device.id).
  • fe3e255 — Stripe Billing Portal route and Account Services Manage billing entry.

Cloud trial policy

Business rule:

  • Every device gets one 30-day Daily Backup Starter Trial on first binding activation.
  • Trial entitlement: cloud_playback quota 1 (24-hour rolling history).
  • Same userId + deviceId repeated 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.ts
  • app/api/devices/activate/route.ts
  • Trial product sku: aovis-daily-backup-starter-trial
  • Subscription external id pattern: device-trial:${deviceId}:${userId}

Plan definitions are centralized in lib/cloud-storage-plans.ts.

PlanPriceRetention / quotaEntitlements
Local$00no cloud checkout
Starter$1.99/month1 daycloud_playback, ai_summary, smart_search
Standard$4.99/month7 dayscloud_playback, ai_summary, smart_search
Premium$9.99/month30 dayscloud_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_playback quota), not AI feature gating.

Entitlement access window

  • lib/cloud-access-window.ts validates requested history range against cloud_playback quota.
  • /api/devices/[id]/clip and on-demand /api/devices/[id]/stream enforce the window.
  • Live stream path is not gated by cloud retention history.
  • lib/aws/device-service-access.ts selects 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.ts
  • lib/aws/kvs-retention.ts
  • scripts/kvs-retention-reconcile.ts
  • docs/cloud-storage-retention.md

Production state:

  • KVS_DATA_RETENTION_HOURS=720 in /opt/aovis/aovis-store-staging/.env.production.
  • aovis-stream-aovis-n4k-000001 verified at 720h.
  • IAM user aovis-backend-service has kinesisvideo:UpdateDataRetention through managed policy aovis-backend-service-policy default version v2.

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 be Device.id, not DeviceOwnership.id.
  • planCodestarter, standard, or premium.

Rules:

  • User must be logged in.
  • User must be OWNER for 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.completed
  • checkout.session.async_payment_succeeded
  • customer.subscription.updated
  • customer.subscription.deleted

Key helpers:

  • mapStripeSubscriptionStatus()
    • active / trialing -> active
    • canceled / incomplete_expired -> canceled
    • past_due, unpaid, incomplete, unknown -> inactive
  • extractStripeSubscriptionPeriod()
    • Uses top-level current_period_start/end when present.
    • Falls back to items.data[0].current_period_start/end.
    • Active/trialing without period throws; do not write now as an active expiry.
    • Canceled/inactive without period can use now for safe expiration.
  • ensureCloudSubscriptionEntitlements() in lib/cloud-subscription-entitlements.ts
    • Upserts Product / Subscription / Entitlements.
    • Active -> entitlement ACTIVE.
    • Canceled/inactive -> Subscription CANCELLED, entitlements EXPIRED.
    • Idempotent for repeated Stripe events.

Verified paid Standard DB state after dry-run:

  • Product: aovis-cloud-storage-standard
  • Subscription: source stripe, status ACTIVE, expires at 2026-06-13 14:42:35.
  • Entitlements:
    • cloud_playback quota=7 ACTIVE, expires 2026-06-13 14:42:35
    • ai_summary ACTIVE, expires 2026-06-13 14:42:35
    • smart_search ACTIVE, expires 2026-06-13 14:42:35
  • Trial quota=1 remains as history and fallback.

Frontend service pages

Files:

  • components/cloud-storage-subscribe-form.tsx
  • app/services/cloud-storage/page.tsx
  • app/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 deviceId and planCode.
  • Multiple owned devices render <select name="deviceId">.
  • Important bug fixed in be17b03: submitted deviceId must be ownedDevices[n].device.id, not ownedDevices[n].id.

Billing Portal

Files:

  • app/api/billing/portal/route.ts
  • app/account/services/page.tsx

Behavior:

  • POST /api/billing/portal creates Stripe Billing Portal session.
  • Only logged-in users can access.
  • Optional local subscriptionId must belong to current user.
  • Only sourceChannel=stripe and 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

  1. Real video cloud storage pipeline:
    • Device/producer push to KVS.
    • Real clip/history listing.
    • Playback UI.
  2. AI summary and smart search implementation:
    • Event indexing.
    • Bedrock/OpenSearch/S3 integration as needed.
  3. 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.
  4. Device transfer / unbind / rebind:
    • Must preserve no-second-free-trial policy.
    • Must define how paid device-scoped subscriptions behave when ownership changes.
  5. 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.