Privacy-first feedback widget, traffic analytics, and developer monitoring. Drop a single script tag on your site to collect customer feedback, capture email subscribers, track visits, and report browser errors -- all backed by a role-aware dashboard and a self-hosted SQLite backend.
- Free for personal and non-revenue projects
- Commercial license required for revenue-generating use
- See LICENSE for details
# Clone and start the development stack
git clone https://github.com/tyemirov/loopaware.git
cd loopaware
./scripts/up.sh
# Open the dashboard
open http://localhost:8080/loginEmbed the feedback widget on any page:
<script src="https://loopaware.mprlab.com/widget.js?site_id=YOUR_SITE_ID" defer></script>- Google Identity Services authentication via TAuth
- Role-aware dashboard (
/app) with admin and creator/owner scopes - YAML configuration for privileged accounts (
configs/config.loopaware.yml) - REST API to create, update, and inspect sites, feedback, subscribers, and traffic
- Background favicon refresh scheduler with live dashboard notifications
- Embeddable JavaScript widget with strict origin validation
- Email subscription capture via an embeddable subscribe form
- Privacy-safe traffic pixel with per-site visit and visitor counts
- Daily, weekly, or monthly traffic report emails delivered through Pinguin
- First-class LA Sentry developer error monitoring with protected server-to-server ingest and origin-bound browser capture
- SQLite-first storage with pluggable drivers
- Public privacy policy and compliance endpoints for visibility
- Table-driven tests and fast in-memory SQLite fixtures
Edit the tracked YAML file at configs/config.loopaware.yml with the email addresses that should receive administrator privileges (the file is optional if you prefer environment-only configuration):
admins:
- temirov@gmail.comLoopAware loads the file specified by --config (default configs/config.loopaware.yml) before starting the HTTP server.
Set the ADMINS environment variable with a comma-separated list (for example ADMINS=alice@example.com,bob@example.com) to override the YAML roster without editing the file. When neither source is present the server starts without administrators and records a warning in the logs.
Backend (cmd/server):
| Variable | Required | Description |
|---|---|---|
SESSION_SECRET |
✅ | 32+ byte secret for subscription confirmation tokens |
TAUTH_BASE_URL |
✅ | Base URL for the TAuth API |
TAUTH_TENANT_ID |
✅ | Tenant identifier configured in TAuth |
TAUTH_JWT_SIGNING_KEY |
✅ | JWT signing key used to validate app_session |
TAUTH_SESSION_COOKIE_NAME |
⚙️ | Session cookie name set by TAuth (defaults to app_session) |
PINGUIN_ADDR |
✅ | Pinguin gRPC address |
PINGUIN_AUTH_TOKEN¹ |
✅ | Bearer token passed to the Pinguin gRPC service |
PINGUIN_TENANT_ID |
✅ | Tenant identifier used when calling the Pinguin gRPC API |
TRAFFIC_REPORT_EMAILS_ENABLED |
⚙️ | Enables scheduled/test traffic report emails (default true) |
ADMINS |
⚙️ | Comma-separated admin emails; overrides the YAML roster |
PUBLIC_BASE_URL |
⚙️ | Frontend origin used for CORS and subscription links |
APP_ADDR |
⚙️ | Listen address (default :8080) |
DB_DRIVER |
⚙️ | Storage driver (sqlite, etc.) |
DB_DSN |
⚙️ | Driver-specific DSN |
Secrets must come from the environment; only non-sensitive settings belong in configs/config.loopaware.yml.
When running via Docker Compose, copy the tracked env templates under configs/ and edit the local .env.* files:
cp configs/.env.loopaware.example configs/.env.loopaware
cp configs/.env.tauth.example configs/.env.tauth
cp configs/.env.pinguin.example configs/.env.pinguin
$EDITOR configs/.env.loopaware configs/.env.tauth configs/.env.pinguin¹Pinguin and LoopAware must share the exact same bearer secret. Provide identical values for GRPC_AUTH_TOKEN and PINGUIN_AUTH_TOKEN, for example:
GRPC_AUTH_TOKEN=loopaware-local-secret
PINGUIN_AUTH_TOKEN=loopaware-local-secretLoopAware falls back to GRPC_AUTH_TOKEN when PINGUIN_AUTH_TOKEN is empty, so exporting the shared value once at runtime also works.
All configuration options are also exposed as Cobra flags:
loopaware --config=configs/config.loopaware.yml \
--app-addr=:8080 \
--db-driver=sqlite \
--db-dsn="file:loopaware.sqlite?_foreign_keys=on" \
--session-secret=$SESSION_SECRET \
--tauth-base-url=$TAUTH_BASE_URL \
--tauth-tenant-id=$TAUTH_TENANT_ID \
--tauth-signing-key=$TAUTH_JWT_SIGNING_KEY \
--tauth-session-cookie-name=$TAUTH_SESSION_COOKIE_NAME \
--traffic-report-emails=true \
--public-base-url=https://feedback.example.com
Flags are optional when the equivalent environment variables are set.
For Docker-based local development, use the helper script:
./scripts/up.shStop the local stack with:
./scripts/down.shscripts/up.sh is the canonical startup path for Dockerized LoopAware. With no argument it opens an interactive selector.
You can also call it explicitly as ./scripts/up.sh local or ./scripts/up.sh computercat.
The local compose stack now includes a gHTTP proxy that serves web/ at http://localhost:8080 and forwards /api,
/auth, /public, and /tauth.js to the backend services. That proxy is also responsible for the browser-facing
security headers on the static HTML and proxied API responses in the local stack.
If you want to run only the API process without Docker, use:
SESSION_SECRET=$(openssl rand -hex 32) \
TAUTH_BASE_URL=http://localhost:8081 \
TAUTH_TENANT_ID=loopaware \
TAUTH_JWT_SIGNING_KEY=replace-with-tauth-jwt-signing-key \
TAUTH_SESSION_COOKIE_NAME=loopaware_development_session \
PUBLIC_BASE_URL=http://localhost:8080 \
go run ./cmd/server --config=configs/config.loopaware.ymlWhen serving the static frontend directly from web/, no preparation step is required. Keep the tracked runtime
config in web/config.yml and serve web/ from the frontend origin or reverse proxy that will answer /config.yml,
/api, and /auth.
Then open /app on that frontend origin to trigger Google Sign-In.
Ensure the TAuth service is running at TAUTH_BASE_URL with a tenant that matches TAUTH_TENANT_ID.
Administrators listed in configs/config.loopaware.yml can manage every site; other users see only the sites they own
or originally created with their Google account.
The static frontend pins mpr-ui and tauth.js through CDN URLs in web/runtime-env.js. Do not copy third-party
browser bundles into web/; non-CDN frontend dependencies are forbidden by architecture.
- Users visit
/login(automatic redirect from protected routes). - TAuth issues the session cookie configured by
TAUTH_SESSION_COOKIE_NAME(defaults toapp_session) via Google Identity Services and keeps it refreshed. api.AuthManagervalidates the session JWT, injects user details into the request context, and enforces admin / owner access.- The dashboard and JSON APIs consume the authenticated context.
LoopAware’s frontend lives in web/ and is hosted separately (CDN or reverse proxy). It includes:
/login— landing page with TAuth-backed Google Sign-In./privacy— static privacy policy linked from the landing and dashboard footers./app— dashboard shell (data loaded via/api/*)./subscriptions/confirmand/subscriptions/unsubscribe— email link pages./widget.js,/subscribe.js,/pixel.js,/la-sentry.js— embeddable JavaScript assets./sentry/errors— protected server-to-server developer error ingest./sentry/browser-errors— origin-bound browser developer error ingest.
The repository does not vendor third-party browser dependencies into web/. External JavaScript and CSS, including UI
libraries, must be referenced through pinned CDN URLs. Any browser dependency that is not delivered by CDN is
forbidden. web/ is reserved for LoopAware-authored assets only, so deployments, cache behavior, and browser tests
exercise the same delivery path used in production.
Set PUBLIC_BASE_URL to the frontend origin so the API emits correct links and CORS allows browser access. Use
absolute data-api-origin attributes (or api_origin query params) on embed scripts when the API runs on a different
origin. The dashboard and login pages call /api and /auth relative to the frontend origin, so split-origin
deployments should use a reverse proxy or update the static HTML in web/ to point at those services.
The tracked runtime host mapping lives in web/config.yml, which web/runtime-env.js fetches directly at runtime.
Canonical SEO metadata, Open Graph URLs, robots.txt, and sitemap.xml are fixed to the single public site
https://loopaware.mprlab.com and are not environment-specific.
Each environment may also define services.siteWidgetSiteId there to bootstrap the first-party feedback widget on
/login and /app without hard-coding a site UUID into the static HTML.
All authenticated endpoints live under /api and require the TAuth session cookie configured by TAUTH_SESSION_COOKIE_NAME. Public collection endpoints for
feedback, subscriptions, and visits do not require a session but still enforce per-site origin rules. JSON responses
include Unix timestamps in seconds.
| Method | Path | Role | Description |
|---|---|---|---|
GET |
/api/me |
any | Current account metadata (email, name, role, avatar.url) |
GET |
/api/sites |
any | Sites visible to the caller (admin = all, user = owned) |
POST |
/api/sites |
any | Create a site (requires name, allowed_origin, owner_email) |
PATCH |
/api/sites/:id |
owner/admin | Update name/origin; admins may reassign ownership |
DELETE |
/api/sites/:id |
owner/admin | Delete a site |
GET |
/api/sites/:id/messages |
owner/admin | List feedback messages (newest first) |
GET |
/api/sites/:id/subscribers |
owner/admin | List subscribers for a site |
GET |
/api/sites/:id/subscribers/export |
owner/admin | Download subscribers as CSV |
PATCH |
/api/sites/:id/subscribers/:subscriber_id |
owner/admin | Update a subscriber’s status (confirm or unsubscribe) |
DELETE |
/api/sites/:id/subscribers/:subscriber_id |
owner/admin | Delete a subscriber |
GET |
/api/sites/:id/visits/stats |
owner/admin | Aggregate visit and unique visitor counts plus recent visits and top pages |
GET |
/api/sites/:id/sentry/issues |
owner/admin | List grouped developer error issues for a site |
GET |
/api/sites/:id/sentry/issues/:issue_id |
owner/admin | Inspect latest and recent LA Sentry error occurrences |
PATCH |
/api/sites/:id/sentry/issues/:issue_id |
owner/admin | Update issue status (unresolved, resolved, or ignored) |
POST |
/api/sites/:id/sentry/token |
owner/admin | Rotate and reveal a per-site LA Sentry ingest token |
GET |
/api/sites/:id/visits/trend |
owner/admin | Daily visit trend (default 7 days, optional days query param up to 30) |
GET |
/api/sites/:id/visits/attribution |
owner/admin | Source/medium/campaign attribution breakdown (optional limit query param up to 50; defaults to 10) |
GET |
/api/sites/:id/visits/engagement |
owner/admin | Visitor engagement metrics (default 30 days, optional days query param up to 90) |
GET |
/api/sites/favicons/events |
any | Server-sent events stream announcing refreshed site favicons |
GET |
/api/sites/feedback/events |
any | Server-sent events stream announcing new feedback |
POST |
/public/feedback |
public | Submit feedback (requires site_id, valid contact as email or phone, and at least one of message or sentiment) |
POST |
/public/subscriptions |
public | Submit an email subscription (JSON body with site_id, email, optional name and source_url) |
POST |
/public/subscriptions/confirm |
public | Confirm a subscription for a given site_id and email |
POST |
/public/subscriptions/unsubscribe |
public | Unsubscribe an email address for a given site_id |
GET |
/public/visits |
public | Record a page visit for a site (returns a 1×1 GIF for use as a tracking pixel) |
POST |
/sentry/errors |
ingest token | Submit developer error events with Authorization: Bearer <token> or X-LoopAware-Sentry-Token |
POST |
/sentry/browser-errors |
site origin | Submit browser JavaScript error events from configured site origins |
Subscriptions use confirmation and unsubscribe links sent via email: the static frontend pages at
/subscriptions/confirm?token=... and /subscriptions/unsubscribe?token=... call the API without requiring browser
origin headers.
LA Sentry ingest accepts JSON with site_id, event_id, timestamp, platform, environment, release, level,
message, exception_type, stacktrace, request, user_hash, tags, and extra. Rotate the per-site token from
the dashboard LA Sentry tab; tokens are shown only once and are intended for server-side clients. The browser harness uses
/sentry/browser-errors without a token. Browser events are accepted only from the site's configured allowed_origin
values, are rate-limited by client IP, and store minimized request metadata.
The allowed_origin field for a site may contain multiple origins separated by spaces or commas (for example https://mprlab.com http://localhost:8080); widgets, subscribe forms, and pixels will accept requests from any configured origin while still rejecting traffic from unknown sites.
The /api/me response includes a role value of admin or user and an avatar.url pointing to the caller's cached
profile image (served from /api/me/avatar). The dashboard uses this payload to render the account card and determine
site scope.
Both roles can create, update, and delete sites. Administrators additionally view every site in the system, while users see only the sites they own or originally created.
Deployments upgraded from versions prior to LA-57 should allow the server startup migration to run once; it backfills any
sites missing a creator_email with temirov@gmail.com to preserve creator-based visibility rules. New site creations
store the authenticated creator separately from the configured owner mailbox.
The Bootstrap front end consumes the APIs above. Features include:
- Account card with avatar, email, and role badge
- Site creation and owner reassignment available to every authenticated user; administrators additionally see all sites
- Owner/admin editor for site metadata
- Widget placement controls that persist the bubble’s side (left/right) and bottom offset without code changes
- Feedback table with human-readable timestamps
- Subscribers panel with per-site subscriber counts, table, CSV export, and a copyable
subscribe.jssnippet - Section selector tabs to switch between Feedback, Subscriptions, and Traffic
- Subscriber deletion via a confirmation modal
- Traffic card with visit and unique visitor counts, recent visits, and a copyable
pixel.jssnippet - Real-time favicon refresh notifications delivered through the SSE stream
- Sign-out button wired to TAuth (
/auth/logout) - Inactivity prompt appears after the configured delay (defaults to 60 seconds) and logs out automatically after the configured timeout (defaults to 120 seconds) if unanswered
The dashboard automatically redirects unauthenticated visitors to /login.
- Create a site (admin) and copy the generated
<script>tag from the API response. - Embed the script on any page served from one of the site’s configured
allowed_originvalues (you can supply multiple origins separated by spaces or commas). Include thedeferattribute so the widget loads without blocking the page; the script waits for the body before rendering the UI. - Visitors can open the floating bubble, submit feedback with a valid email or phone plus a message and/or sentiment, and the messages appear under
/api/sites/:id/messagesand in the dashboard.
Example snippet (replace the base URL with your LoopAware deployment and the site identifier with the value returned by the API):
<script defer src="https://loopaware.mprlab.com/widget.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f"></script>Each site exposes a subscribe snippet that renders an email capture form and posts subscriptions to /public/subscriptions.
-
In the dashboard, select a site and use the Subscribers panel to copy the subscribe snippet.
-
Embed the script on pages served from any of the site’s
allowed_originentries. The basic form looks like:<script defer src="https://loopaware.mprlab.com/subscribe.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f"></script>
-
Optional query parameters let you adjust behavior and styling:
mode=inline(default) ormode=bubblefor a floating button.accent=#0d6efdto override the accent color.cta=Subscribeto customize the button text.success=You%27re+on+the+list%21anderror=Please+try+again.for inline messages.name_field=falseto hide the optional name field.
The form enforces the site’s allowed_origin list using request headers and source_url and responds with inline success or
error messages so visitors never leave the page.
The traffic pixel records page visits per site and powers the dashboard Traffic card and top-pages table.
-
In the dashboard, select a site and use the Traffic panel to copy the pixel snippet.
-
Embed the script on every page served from any of the site’s
allowed_originentries:<script defer src="https://loopaware.mprlab.com/pixel.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f"></script>
-
On load,
pixel.jssends a beacon to/public/visitswith the site ID, current URL, referrer, and a stable visitor ID stored inlocalStorage. Requests from origins outside the site’sallowed_originlist are rejected. Traffic from known bot user-agent signatures is stored but excluded from default dashboard totals, top-page rankings, trends, and attribution and engagement breakdowns.
For non-JavaScript environments you can fall back to a plain image pixel:
<img src="https://loopaware.mprlab.com/public/visits?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f&url=https%3A%2F%2Fexample.com%2F" alt="" width="1" height="1" />Server-side clients should use the protected /sentry/errors endpoint with a per-site ingest token. The repository
includes first-party Go and Python clients:
- Go:
pkg/lasentry - Python:
clients/python/la_sentry
Browser pages can use the standalone harness without exposing the server-side token:
<script defer src="https://loopaware.mprlab.com/la-sentry.js?site_id=6f50b5f4-8a8f-4e4a-9d69-1b2a3c4d5e6f&environment=production&release=2026.04.24"></script>The browser harness installs window.LASentry.captureError(error, attrs) and automatically captures uncaught
error and unhandledrejection events. It sends sanitized URL/referrer/user-agent metadata, stack frames, tags, and
explicit extra values supplied by application code.
make format
make lint
make testmake test runs the Playwright integration suite against tests/docker-compose.yml, with test-owned env fixtures under
tests/configs/. That stack builds the API image, serves web/ via gHTTP, and exercises both UI and /api/* flows.
Use make test-unit for Go-only tests and make test-integration-api to focus on API specs. Playwright artifacts
(traces, screenshots, videos) land under tests/test-results/ on failure. The integration runner tears its compose
project down on exit, including failures and signal exits.
GitHub Pages and Docker release publishing are tag-driven and run only for pushed tags that match vMAJOR.MINOR.PATCH.
GitHub Pagesdeploys the trackedweb/tree from the tagged commit.Build and Publish Docker Imagepushes:ghcr.io/<owner>/loopaware:latestghcr.io/<owner>/loopaware:<tag>ghcr.io/<owner>/loopaware:<sha>
Use a semantic version tag:
git tag v0.1.0
git push origin v0.1.0Tags that do not match vMAJOR.MINOR.PATCH are rejected by workflow validation and will not publish release artifacts.
The previous Docker and Compose files remain compatible. Ensure the container receives the OAuth environment variables
and mounts configs/config.loopaware.yml containing the admin roster.
cp configs/.env.loopaware.example configs/.env.loopaware
cp configs/.env.tauth.example configs/.env.tauth
cp configs/.env.pinguin.example configs/.env.pinguin
$EDITOR configs/.env.loopaware configs/.env.tauth configs/.env.pinguin
./scripts/up.shThe compose file binds configs/config.loopaware.yml into the LoopAware container at /app/configs/config.loopaware.yml
and loads per-service environment variables via env_file from configs/.env.*.
The container now runs as root so the SQLite data volume remains writable; if you need to switch back to an unprivileged
user, update the Docker image to chown the mounted directory before starting the binary.
For the computercat TLS stack, use:
./scripts/up.sh computercat