@lightupai/polaris 0.0.50 → 0.0.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Makefile CHANGED
@@ -17,20 +17,23 @@ dev-down:
17
17
  docker compose down
18
18
 
19
19
  # Cloud service API (port 4321)
20
+ # nohup so the service survives `make` exiting. A bare `&` leaves it as an
21
+ # orphaned job that receives SIGHUP and dies (banner prints, then it's gone);
22
+ # nohup makes it ignore SIGHUP. Logs go to /tmp/polaris-*.log.
20
23
  api:
21
24
  @echo "Starting API server on http://localhost:4321"
22
- @npx bun run src/service/server.ts 2>/tmp/polaris-api.log &
25
+ @nohup npx bun run src/service/server.ts >/tmp/polaris-api.log 2>&1 &
23
26
 
24
27
  # Web app (port 3000)
25
28
  web:
26
29
  @echo "Starting web app on http://localhost:3000"
27
- @npx bun --hot run src/web/serve.ts &
30
+ @nohup npx bun --hot run src/web/serve.ts >/tmp/polaris-web.log 2>&1 &
28
31
 
29
32
  # Local daemon (port 4322) — uses local profile token if available
30
33
  daemon:
31
34
  @echo "Starting daemon on http://127.0.0.1:4322"
32
35
  @TOKEN=$$(jq -r '.profiles.local.token // empty' ~/.polaris/config.json 2>/dev/null || echo ""); \
33
- POLARIS_DAEMON_PORT=4322 POLARIS_SERVICE_URL=http://localhost:4321 POLARIS_AUTH_TOKEN="$$TOKEN" npx bun run src/daemon/daemon.ts &
36
+ POLARIS_DAEMON_PORT=4322 POLARIS_SERVICE_URL=http://localhost:4321 POLARIS_AUTH_TOKEN="$$TOKEN" nohup npx bun run src/daemon/daemon.ts >/tmp/polaris-daemon.log 2>&1 &
34
37
 
35
38
  # Slack bridge (auto-detects org from DB, needs SLACK_APP_TOKEN in .env)
36
39
  bridge:
@@ -38,7 +41,7 @@ bridge:
38
41
  ORG=$$(docker exec collab-polaris-postgres-1 psql -U polaris -d polaris -t -A -c "SELECT id FROM orgs WHERE slack_team_id IS NOT NULL LIMIT 1;" 2>/dev/null); \
39
42
  if [ -n "$$ORG" ]; then \
40
43
  echo "Starting Slack bridge for org $$ORG"; \
41
- npx bun run src/slack/bridge.ts $$ORG 2>/tmp/polaris-bridge.log & \
44
+ nohup npx bun run src/slack/bridge.ts $$ORG >/tmp/polaris-bridge.log 2>&1 & \
42
45
  else echo "Skipping bridge (no Slack-connected org found)"; fi; \
43
46
  fi
44
47
 
@@ -0,0 +1,167 @@
1
+ # Signup Flow — Design & Todos
2
+
3
+ ## Current State
4
+
5
+ User clicks "Get started" on the landing page → Google OAuth → user/org created in DB → redirect to dashboard. No plan differentiation, no onboarding, no Slack connection prompt. New users land on an empty dashboard with no guidance.
6
+
7
+ ## Target State
8
+
9
+ A user who signs up should experience a smooth path from landing page to their first live session:
10
+
11
+ 1. Choose a plan (or default to free)
12
+ 2. Sign up with Google
13
+ 3. Connect their Slack workspace
14
+ 4. Install the CLI
15
+ 5. Join a channel and start their first session
16
+
17
+ Each step should feel intentional, not accidental. The user should never be left wondering "what do I do next?"
18
+
19
+ ---
20
+
21
+ ## Design
22
+
23
+ ### Plan Selection → Signup
24
+
25
+ The pricing cards on the landing page link to `/signup?plan=free` or `/signup?plan=team`. Enterprise goes to `mailto:support@withpolaris.ai`. The plan parameter is stored in the OAuth state, survives the Google redirect, and is persisted on the org record after signup.
26
+
27
+ If no plan is specified (e.g., user clicks the nav "Sign up" button), default to `free`.
28
+
29
+ ### Post-Signup Routing
30
+
31
+ After Google OAuth completes, the callback handler should route based on the user's state:
32
+
33
+ | Scenario | Route to |
34
+ |---|---|
35
+ | Existing user (login) | Dashboard |
36
+ | New user, existing org | Dashboard (org already set up) |
37
+ | New user, new org, no Slack | Onboarding: Connect Slack |
38
+ | New user, new org, Slack connected | Onboarding: Install CLI |
39
+
40
+ ### Onboarding Flow
41
+
42
+ A dedicated onboarding page replaces the empty dashboard for new orgs. Three steps, shown as a checklist:
43
+
44
+ **Step 1: Connect Slack**
45
+ - Large "Add to Slack" button
46
+ - Explain: "Polaris uses Slack as the collaboration layer. Connect your workspace to get started."
47
+ - After connecting, auto-advance to Step 2
48
+
49
+ **Step 2: Install the CLI**
50
+ - Show `npx @lightupai/polaris` with a copy button
51
+ - Explain: "Run this on your machine to set up hooks and log in."
52
+ - A "I've installed it" button to advance (or auto-detect via CLI auth callback)
53
+
54
+ **Step 3: Start a session**
55
+ - Show `/polaris join #your-channel` with a copy button
56
+ - Explain: "Run this in Claude Code to start streaming your session."
57
+ - Link to dashboard
58
+
59
+ The existing `renderWelcomePage` and `renderSetupView` functions can be repurposed for this flow.
60
+
61
+ ### Welcome Email
62
+
63
+ After a new user signs up, send a welcome email to their address from `hello@withpolaris.ai`:
64
+
65
+ - Welcome to Polaris
66
+ - Quick start: Connect Slack → Install CLI → Join a channel
67
+ - Link to community (GitHub Discussions)
68
+ - Link to support (support@withpolaris.ai)
69
+
70
+ Requires an email sending service (Resend, SendGrid, or Postmark). Cloudflare email routing only handles inbound — outbound needs a transactional email provider.
71
+
72
+ ### Plan Enforcement
73
+
74
+ Plans are stored on the org record. Limits are enforced at the event ingestion layer:
75
+
76
+ | | Free | Team | Enterprise |
77
+ |---|---|---|---|
78
+ | Users | Unlimited | Unlimited | Unlimited |
79
+ | Prompts/month | 1,000 | 10,000 | Custom |
80
+ | Data captured | 5 GB | 50 GB | Custom |
81
+ | Retention | 7 days | 90 days | Custom |
82
+
83
+ **Enforcement approach:**
84
+ - Count user prompts per org per calendar month (query events table)
85
+ - Track cumulative data volume per org per month
86
+ - Run a daily cleanup job to delete events beyond the retention window
87
+ - When a limit is reached, soft-block: stop capturing new events but keep Slack streaming active. Show upgrade prompt in dashboard and optionally in Slack.
88
+
89
+ ### Upgrade Flow
90
+
91
+ When a free-tier org approaches or hits their limit:
92
+
93
+ 1. Dashboard shows a usage bar with current/max for prompts and data
94
+ 2. At 80%, show a yellow warning: "You've used 80% of your free prompts this month"
95
+ 3. At 100%, show upgrade prompt: "You've reached your free plan limit. Upgrade to Team for 10x capacity."
96
+ 4. Upgrade button links to Stripe Checkout for the $49/mo Team plan
97
+ 5. After successful payment, Stripe webhook updates the org's plan to `team`
98
+
99
+ ### Billing (Stripe)
100
+
101
+ **Setup needed:**
102
+ - Stripe product: "Polaris Team"
103
+ - Stripe price: $49/month, recurring
104
+ - Checkout session: created when user clicks "Upgrade" or selects Team plan during signup
105
+ - Webhook endpoint: `/stripe/webhook` to handle `checkout.session.completed`, `customer.subscription.deleted`, `invoice.payment_failed`
106
+ - Org record fields: `stripe_customer_id`, `stripe_subscription_id`, `plan`, `plan_status`
107
+
108
+ **Downgrade:**
109
+ - User cancels in Stripe customer portal
110
+ - Webhook fires `customer.subscription.deleted`
111
+ - Org plan reverts to `free`
112
+ - Free plan limits take effect at next billing cycle
113
+
114
+ ---
115
+
116
+ ## Todos
117
+
118
+ ### Phase 1: Foundation (do first)
119
+
120
+ - [ ] **Store selected plan on org record**
121
+ Add a `plan` column to the orgs table (free/team/enterprise, default 'free'). Populate from the `?plan` query param during signup. Show current plan on the dashboard.
122
+
123
+ - [ ] **Prompt Slack connection immediately after signup**
124
+ For new orgs without Slack connected, route to setup view with prominent "Add to Slack" button instead of empty dashboard. Ensure the flow from signup → setup is seamless.
125
+
126
+ - [ ] **Post-signup onboarding flow**
127
+ After a brand new org signup, redirect to onboarding page (not dashboard). Three steps: Connect Slack → Install CLI → Join a channel. Repurpose existing `renderWelcomePage`/`renderSetupView`. Auto-advance when each step is completed.
128
+
129
+ ### Phase 2: Billing
130
+
131
+ - [ ] **Stripe integration for Team plan billing**
132
+ Stripe product/price setup, checkout session on Team plan selection, webhook handler for payment events, store subscription status on org. Handle both new signups selecting Team and existing free users upgrading.
133
+
134
+ - [ ] **Enforce free tier usage limits**
135
+ Prompt counter per org per month, data volume tracking, auto-cleanup of events beyond retention window. Soft-block at limits (stop capture, keep streaming, show upgrade prompt).
136
+
137
+ - [ ] **Upgrade prompt when limits are reached**
138
+ Usage bars on dashboard, warning at 80%, upgrade prompt at 100%. Link to Stripe checkout. Also notify in Slack when org approaches limit.
139
+
140
+ - [ ] **Plan management page in dashboard**
141
+ Show current plan, usage stats (prompts, data, retention), upgrade/downgrade buttons, Stripe customer portal link. Accessible from profile dropdown.
142
+
143
+ ### Phase 3: Polish
144
+
145
+ - [ ] **Welcome email on signup**
146
+ Pick a transactional email service (Resend recommended — simple API, good deliverability, free tier). Send welcome email with quick-start steps and community link. Send from hello@withpolaris.ai.
147
+
148
+ ---
149
+
150
+ ## Dependencies
151
+
152
+ ```
153
+ Store plan on org ──→ Enforce limits ──→ Upgrade prompt
154
+ ──→ Stripe billing ──→ Plan management page
155
+ ──→ Onboarding flow
156
+
157
+ Prompt Slack connection ──→ Onboarding flow
158
+
159
+ Welcome email (independent — needs email service selection)
160
+ ```
161
+
162
+ ## Open Questions
163
+
164
+ - Should we allow plan changes mid-month, or only at billing cycle boundaries?
165
+ - Should free-tier users who hit limits lose access to existing session data, or just stop capturing new data?
166
+ - Do we want a 14-day free trial of Team, or keep it strictly free-then-pay?
167
+ - Should the CLI install step in onboarding auto-detect completion (via the `/auth/cli` callback), or require manual confirmation?
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightupai/polaris",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "polaris": "bin/polaris",
package/src/web/app.ts CHANGED
@@ -36,13 +36,14 @@ import {
36
36
 
37
37
  const SIGNUP_CHANNEL = "#alerts-mql-stream";
38
38
 
39
- function notifySignup(opts: { name: string; email: string; domain: string; orgName: string; isNewOrg: boolean }): void {
39
+ function notifySignup(opts: { name: string; email: string; domain: string; orgName: string; isNewOrg: boolean; plan?: string }): void {
40
40
  const botToken = process.env.SIGNUP_SLACK_BOT_TOKEN;
41
41
  if (!botToken) return;
42
42
 
43
43
  const emoji = opts.isNewOrg ? ":tada:" : ":wave:";
44
44
  const action = opts.isNewOrg ? "signed up (new org)" : "joined";
45
- const text = `${emoji} *${opts.name}* (${opts.email}) ${action} ${opts.orgName} (${opts.domain})`;
45
+ const planTag = opts.plan ? ` [${opts.plan}]` : "";
46
+ const text = `${emoji} *${opts.name}* (${opts.email}) ${action} — ${opts.orgName} (${opts.domain})${planTag}`;
46
47
 
47
48
  fetch("https://slack.com/api/chat.postMessage", {
48
49
  method: "POST",
@@ -91,7 +92,7 @@ function getGoogle(): Google {
91
92
  );
92
93
  }
93
94
 
94
- const oauthStates = new Map<string, { type: "login" | "signup"; codeVerifier: string; timestamp: number }>();
95
+ const oauthStates = new Map<string, { type: "login" | "signup"; codeVerifier: string; timestamp: number; plan?: string }>();
95
96
  const cliCallbackPorts = new Map<string, number>(); // state → CLI local server port
96
97
 
97
98
  // If this auth flow was initiated by the CLI, redirect the token to the CLI's local server.
@@ -128,11 +129,12 @@ export function createApp(sql: Sql) {
128
129
 
129
130
  // --- Auth: single Google SSO flow for both signup and login ---
130
131
 
131
- function startGoogleAuth(c: { redirect: (url: string) => Response }) {
132
+ function startGoogleAuth(c: { req: { query: (k: string) => string | undefined }; redirect: (url: string) => Response }) {
132
133
  const google = getGoogle();
133
134
  const state = crypto.randomUUID();
134
135
  const codeVerifier = crypto.randomUUID();
135
- oauthStates.set(state, { type: "login", codeVerifier, timestamp: Date.now() });
136
+ const plan = c.req.query("plan");
137
+ oauthStates.set(state, { type: "login", codeVerifier, timestamp: Date.now(), plan });
136
138
  const url = google.createAuthorizationURL(state, codeVerifier, ["openid", "email", "profile"]);
137
139
  return c.redirect(url.toString());
138
140
  }
@@ -195,7 +197,7 @@ export function createApp(sql: Sql) {
195
197
  }).catch(() => {});
196
198
 
197
199
  // Notify internal team
198
- notifySignup({ name, email, domain, orgName: existingOrg.name, isNewOrg: false });
200
+ notifySignup({ name, email, domain, orgName: existingOrg.name, isNewOrg: false, plan: stateData.plan });
199
201
 
200
202
  const token = await createToken({ sub: userId, email, name, org_id: existingOrg.id, participant_id: participantId });
201
203
  return authRedirect(c, state, token);
@@ -214,7 +216,7 @@ export function createApp(sql: Sql) {
214
216
  await createUser(sql, userId, email, name, orgId, participantId);
215
217
 
216
218
  // Notify internal team
217
- notifySignup({ name, email, domain, orgName, isNewOrg: true });
219
+ notifySignup({ name, email, domain, orgName, isNewOrg: true, plan: stateData.plan });
218
220
 
219
221
  const token = await createToken({ sub: userId, email, name, org_id: orgId, participant_id: participantId });
220
222
  return authRedirect(c, state, token);
package/src/web/pages.ts CHANGED
@@ -375,7 +375,7 @@ export function renderLandingPage(): string {
375
375
  <span class="text-gray-500 text-sm ml-1">forever</span>
376
376
  </div>
377
377
  <p class="mt-2 text-sm text-gray-500">For individuals and small teams getting started.</p>
378
- <a href="/signup" class="mt-6 block w-full text-center px-4 py-2.5 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition">Get started</a>
378
+ <a href="/signup?plan=free" class="mt-6 block w-full text-center px-4 py-2.5 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition">Get started</a>
379
379
  <ul class="mt-6 space-y-3 text-sm text-gray-700">
380
380
  <li class="flex items-start gap-2"><svg class="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>Unlimited users</li>
381
381
  <li class="flex items-start gap-2"><svg class="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg><span><strong>1,000</strong> prompts / month</span></li>
@@ -393,7 +393,7 @@ export function renderLandingPage(): string {
393
393
  <span class="text-gray-500 text-sm ml-1">/month</span>
394
394
  </div>
395
395
  <p class="mt-2 text-sm text-gray-500">For teams that need more capacity, history, and search.</p>
396
- <a href="/signup" class="mt-6 block w-full text-center px-4 py-2.5 bg-polaris-700 text-white text-sm font-medium rounded-lg hover:bg-polaris-800 transition">Get started</a>
396
+ <a href="/signup?plan=team" class="mt-6 block w-full text-center px-4 py-2.5 bg-polaris-700 text-white text-sm font-medium rounded-lg hover:bg-polaris-800 transition">Get started</a>
397
397
  <ul class="mt-6 space-y-3 text-sm text-gray-700">
398
398
  <li class="flex items-start gap-2"><svg class="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>Unlimited users</li>
399
399
  <li class="flex items-start gap-2"><svg class="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg><span><strong>10,000</strong> prompts / month</span></li>
@@ -410,7 +410,7 @@ export function renderLandingPage(): string {
410
410
  <span class="text-4xl font-bold text-gray-900">Custom</span>
411
411
  </div>
412
412
  <p class="mt-2 text-sm text-gray-500">For organizations with compliance and scale needs.</p>
413
- <a href="mailto:hello@withpolaris.ai" class="mt-6 block w-full text-center px-4 py-2.5 bg-white border border-gray-300 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-50 transition">Contact us</a>
413
+ <a href="mailto:support@withpolaris.ai?subject=Enterprise%20plan%20inquiry" class="mt-6 block w-full text-center px-4 py-2.5 bg-white border border-gray-300 text-gray-700 text-sm font-medium rounded-lg hover:bg-gray-50 transition">Contact us</a>
414
414
  <ul class="mt-6 space-y-3 text-sm text-gray-700">
415
415
  <li class="flex items-start gap-2"><svg class="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>Everything in Team</li>
416
416
  <li class="flex items-start gap-2"><svg class="w-4 h-4 text-green-500 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>Custom usage limits</li>