-
Notifications
You must be signed in to change notification settings - Fork 2
user tickets SETUP
For AI-assisted one-shot setup, see CLAUDE_AI_SETUP.md
- Kotlin Multiplatform project with Compose Multiplatform
- Supabase project (URL + anon key) — one dedicated project per app
- Koin for dependency injection
- Jetpack Navigation (type-safe,
androidx.navigation:navigation-compose)
# gradle/libs.versions.toml
[versions]
kmptoolkit-product-tickets = "3.0.0"
[libraries]
kmptoolkit-product-tickets = { module = "io.github.mobilebytelabs:kmptoolkit-product-tickets", version.ref = "kmptoolkit-product-tickets" }// shared/build.gradle.kts (or your KMP module)
commonMain.dependencies {
implementation(libs.kmptoolkit.product.tickets)
}Run in your Supabase project's SQL Editor:
-- Table (no product_type — each app has its own Supabase project)
CREATE TABLE IF NOT EXISTS product_tickets (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
ticket_type TEXT NOT NULL DEFAULT 'feature_request',
title TEXT NOT NULL,
description TEXT NOT NULL,
category TEXT DEFAULT 'general',
status TEXT DEFAULT 'pending',
priority TEXT DEFAULT 'medium',
platform TEXT,
app_version TEXT,
milestone TEXT,
labels JSONB DEFAULT '[]',
attachments JSONB DEFAULT '[]',
is_private BOOLEAN DEFAULT false,
user_id TEXT,
user_email TEXT,
device_info TEXT,
upvotes INT DEFAULT 0,
admin_response TEXT,
responded_at TIMESTAMPTZ,
severity TEXT,
resolution TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_product_tickets_status ON product_tickets(status);
CREATE INDEX IF NOT EXISTS idx_product_tickets_user ON product_tickets(user_id);
CREATE INDEX IF NOT EXISTS idx_product_tickets_type ON product_tickets(ticket_type);
-- Row Level Security
ALTER TABLE product_tickets ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can submit public tickets"
ON product_tickets FOR INSERT
WITH CHECK (is_private = false OR user_id IS NOT NULL);
CREATE POLICY "Anyone can read public tickets"
ON product_tickets FOR SELECT
USING (is_private = false);
CREATE POLICY "Users can read their own private tickets"
ON product_tickets FOR SELECT
USING (is_private = true AND user_id = auth.uid()::text);
-- Toggle vote RPC (unique per-user via ticket_votes table)
CREATE TABLE IF NOT EXISTS ticket_votes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
ticket_id UUID NOT NULL REFERENCES product_tickets(id) ON DELETE CASCADE,
voter_id TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(ticket_id, voter_id)
);
CREATE OR REPLACE FUNCTION toggle_vote(p_ticket_id UUID, p_voter_id TEXT)
RETURNS INT LANGUAGE plpgsql SECURITY DEFINER AS $$
DECLARE
v_count INT;
BEGIN
IF EXISTS (SELECT 1 FROM ticket_votes WHERE ticket_id = p_ticket_id AND voter_id = p_voter_id) THEN
DELETE FROM ticket_votes WHERE ticket_id = p_ticket_id AND voter_id = p_voter_id;
UPDATE product_tickets SET upvotes = upvotes - 1, updated_at = NOW() WHERE id = p_ticket_id;
ELSE
INSERT INTO ticket_votes (ticket_id, voter_id) VALUES (p_ticket_id, p_voter_id);
UPDATE product_tickets SET upvotes = upvotes + 1, updated_at = NOW() WHERE id = p_ticket_id;
END IF;
SELECT upvotes INTO v_count FROM product_tickets WHERE id = p_ticket_id;
RETURN v_count;
END;
$$;
-- Add comment RPC
CREATE TABLE IF NOT EXISTS ticket_comments (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
ticket_id UUID NOT NULL REFERENCES product_tickets(id) ON DELETE CASCADE,
author_type TEXT NOT NULL DEFAULT 'user',
author_name TEXT NOT NULL DEFAULT 'Anonymous',
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION add_comment(
p_ticket_id UUID,
p_author_type TEXT,
p_author_name TEXT,
p_content TEXT
) RETURNS void LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
INSERT INTO ticket_comments (ticket_id, author_type, author_name, content)
VALUES (p_ticket_id, p_author_type, p_author_name, p_content);
END;
$$;Team note: If your team has added extra columns to
product_tickets, they are safe — this SQL usesIF NOT EXISTSand will not drop or alter existing columns.
Call this once at app startup, before any UI is shown:
// Android: Application.onCreate()
// iOS: AppDelegate / @main App
// KMP shared: app initialization block
import com.mobilebytelabs.producttickets.config.ProductTicketsConfig
ProductTicketsConfig.init(
supabaseUrl = "https://your-project.supabase.co",
supabaseAnonKey = "your-anon-key",
userId = currentUser?.id, // optional — enables Contact Support + My Tickets
)Note: There is no
productTypeparameter in v3.0.0. Each app has its own Supabase project, so isolation is handled at the project level.
userId is optional. When provided:
- Contact Support tickets are private and visible only to that user
- "My Tickets" tab shows the user's own support messages
import com.mobilebytelabs.producttickets.di.productTicketsModule
// In your Koin startKoin block:
startKoin {
modules(
productTicketsModule,
// ... your other modules
)
}Register all three destinations in your NavHost:
import com.mobilebytelabs.producttickets.ui.productTicketsDestination
import com.mobilebytelabs.producttickets.ui.createTicketDestination
import com.mobilebytelabs.producttickets.ui.ticketDetailDestination
import com.mobilebytelabs.producttickets.ui.navigateToProductTickets
import com.mobilebytelabs.producttickets.ui.navigateToCreateTicket
import com.mobilebytelabs.producttickets.ui.navigateToTicketDetail
import com.mobilebytelabs.producttickets.domain.model.TicketType
NavHost(navController = navController, startDestination = HomeRoute) {
// ... your other destinations
productTicketsDestination(
onBackClick = { navController.popBackStack() },
onNavigateToCreateTicket = { type -> navController.navigateToCreateTicket(type) },
onNavigateToTicketDetail = { id -> navController.navigateToTicketDetail(id) },
)
createTicketDestination(
onBackClick = { navController.popBackStack() },
)
ticketDetailDestination(
onBackClick = { navController.popBackStack() },
)
}Navigate from anywhere in your app:
// Open the tickets screen (Feature Requests / Bug Reports tabs)
navController.navigateToProductTickets()
// Open create form for a specific type
navController.navigateToCreateTicket(TicketType.FEATURE_REQUEST)
navController.navigateToCreateTicket(TicketType.BUG_REPORT)
navController.navigateToCreateTicket(TicketType.CONTACT_SUPPORT)
// Open ticket detail
navController.navigateToTicketDetail(ticketId)After integration, verify:
- App builds without errors
-
product_ticketstable exists in Supabase with all 23 columns -
ticket_votesandticket_commentstables exist -
toggle_voteandadd_commentRPCs exist in Supabase -
ProductTicketsConfig.init()called before navigation -
productTicketsModulein Koin modules list - All 3 nav destinations registered in NavHost
- Navigating to tickets screen shows the screen (not a crash)
- Submitting a test ticket appears in Supabase dashboard
- Upvoting a ticket increments the count
Or run the AI check:
/lib-sync cmp-product-tickets --check
| Error | Cause | Fix |
|---|---|---|
ProductTicketsConfig not initialized |
init() called too late or not at all |
Call before Koin starts or in Application.onCreate |
No destination for ProductTicketsRoute |
Missing productTicketsDestination in NavHost |
Add all 3 destinations |
Tickets not loading |
Supabase RLS or wrong project URL | Check RLS policies + SUPABASE_URL matches this app's project |
Toggle vote fails |
toggle_vote RPC or ticket_votes table missing |
Run Step 2 SQL again |
Contact Support tickets missing |
userId is null |
Pass userId in ProductTicketsConfig.init()
|
The /tickets CLI skill, dashboard, and migrations require keys in a .env file in your project root.
| Key | Purpose | Where to Get |
|---|---|---|
SUPABASE_URL |
Your Supabase project URL | Dashboard → Settings → API → Project URL |
SUPABASE_SERVICE_ROLE_KEY |
Service role key (bypasses RLS — admin only) | Dashboard → Settings → API → service_role |
SUPABASE_DB_URL |
Direct database connection (for migrations) | Dashboard → Settings → Database → Connection string → Session mode |
# .env (project root — add to .gitignore!)
SUPABASE_URL=https://your-ref.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJ...
# Required only for running migrations with psql
SUPABASE_DB_URL=postgresql://postgres.your-ref:your-password@aws-0-region.pooler.supabase.com:5432/postgresAlways use the session pooler (port 5432) for migrations — never the transaction pooler (port 6543):
# Session pooler — port 5432 — supports DDL + transactions (CORRECT)
postgresql://postgres.{ref}:{password}@aws-0-{region}.pooler.supabase.com:5432/postgres
# Transaction pooler — port 6543 — DDL limited (DO NOT use for migrations)
postgresql://postgres.{ref}:{password}@aws-0-{region}.pooler.supabase.com:6543/postgres# Run a migration file with full error safety
psql "${SUPABASE_DB_URL}" \
-v ON_ERROR_STOP=1 \
-1 \
-f migrations/001_initial_schema.sql-
-v ON_ERROR_STOP=1— halt immediately on any SQL error -
-1— wrap entire file in a transaction (all-or-nothing rollback on failure)
- Run
/lib-sync cmp-product-tickets --checkto verify your integration is current with the latest library version - See CLAUDE_AI_SETUP.md to automate setup and keep in sync with AI
- Library contributors: see LIBRARY_DEV.md
** Partials**
App Intents
Bubble
Clipboard
Cookbook
- Clipboard Copy Text
- Clipboard Read Text
- Consumer Anon Key Setup
- Crashlytics Attribution Per Library
- Ifonline Block
- Index
- Index
- Index
- Index
- Open Url Compose
- Pick And Share Image
- React To Offline
- Register Firebase Hooks
- Share Pdf Android
- Share Text
- Wifi Vs Cellular
Firebase Analytics
In App Update
Intent Launcher
Inter App Comms
Modules
- Cmp App Intents
- Cmp App Intents Compose
- Cmp Bubble
- Cmp Clipboard
- Cmp Deep Link
- Cmp Firebase Analytics
- Cmp In App Update
- Cmp Intent Launcher
- Cmp Intent Launcher Compose
- Cmp Library
- Cmp Network Monitor
- Cmp Network Monitor Compose
- Cmp Observe
- Cmp Observe Koin
- Cmp Open Url
- Cmp Pdf Generator
- Cmp Product Tickets
- Cmp Remote Config
- Cmp Share
- Cmp Share Compose
- Cmp Toast
Network Monitor
Open Url
Pdf Generator
Remote Config
Share
Toast
User Tickets
General