@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 +7 -4
- package/docs/signup-flow.md +167 -0
- package/package.json +1 -1
- package/src/web/app.ts +9 -7
- package/src/web/pages.ts +3 -3
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
|
|
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
|
|
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
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
|
|
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
|
-
|
|
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:
|
|
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>
|