mirror of
https://github.com/Rohithgilla12/data-peek
synced 2026-04-21 12:57:16 +00:00
feat: update licensing changes
This commit is contained in:
parent
e817501b6b
commit
82058dc7a7
13 changed files with 176 additions and 2950 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,2 +1,5 @@
|
|||
node_modules/
|
||||
dist/
|
||||
dist/
|
||||
|
||||
# Business documentation (contains sensitive info)
|
||||
docs/
|
||||
47
LICENSE
Normal file
47
LICENSE
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2024 Rohith Gilla
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## Commercial Use License
|
||||
|
||||
While the source code is MIT licensed, pre-built binaries require a license
|
||||
for commercial use. Commercial use is defined as using data-peek for
|
||||
revenue-generating or work-related activities within a for-profit organization
|
||||
of two or more people.
|
||||
|
||||
### Free Use (No License Required)
|
||||
- Personal projects and learning
|
||||
- Open source contributors
|
||||
- Students and educators
|
||||
- Registered non-profit organizations
|
||||
- Solo founders (company of one)
|
||||
|
||||
### License Required
|
||||
- Developers at companies with 2+ people
|
||||
- Freelancers billing clients
|
||||
- Agencies using for client work
|
||||
|
||||
This licensing model is inspired by Yaak and other sustainable indie software
|
||||
projects that balance open source values with sustainable development.
|
||||
|
||||
Learn more at https://data-peek.dev/license
|
||||
|
|
@ -7,42 +7,52 @@ const faqs = [
|
|||
{
|
||||
question: 'Is data-peek really free?',
|
||||
answer:
|
||||
'Yes! The free tier is fully functional with 2 connections, 50 query history items, and 3 tabs. No credit card required, no time limits. The Pro license unlocks unlimited everything plus advanced features like inline editing and query plans.',
|
||||
'Yes! data-peek is free for personal use with all features unlocked. No credit card required, no time limits, no feature restrictions. A license is only required if you use it for commercial purposes at a for-profit company with 2+ people.',
|
||||
},
|
||||
{
|
||||
question: 'What does "pay once, own forever" mean?',
|
||||
question: 'What counts as commercial use?',
|
||||
answer:
|
||||
'Unlike subscription software, you pay $29 once and get a perpetual license. The license includes 1 year of updates. After that year, you can keep using the version you have forever, or renew for another year of updates at a discounted rate.',
|
||||
'Commercial use means using data-peek for work-related activities in a for-profit organization of 2+ people. This includes developers at startups/companies, freelancers billing clients, and agencies doing client work. Solo founders (company of one) are free!',
|
||||
},
|
||||
{
|
||||
question: 'How many devices can I use with one license?',
|
||||
question: 'Is data-peek open source?',
|
||||
answer:
|
||||
'One Pro license includes 3 device activations. You can use data-peek on your work laptop, home computer, and one more device. Need more? Contact us for volume licensing.',
|
||||
'Yes! The source code is MIT licensed on GitHub. You can view, modify, fork, and build it yourself for any purpose. Pre-built binaries require a license for commercial use — this is how we sustain development.',
|
||||
},
|
||||
{
|
||||
question: 'Does data-peek work offline?',
|
||||
question: 'What does "perpetual fallback" mean?',
|
||||
answer:
|
||||
"Absolutely. data-peek runs entirely on your machine. We validate licenses online during activation, but after that, you can work offline. There's a grace period for license revalidation.",
|
||||
'When you buy a Pro license ($29/year), you get 1 year of updates. If you don\'t renew, you keep your current version forever — it doesn\'t stop working. You just won\'t receive future updates. Renew anytime to get the latest.',
|
||||
},
|
||||
{
|
||||
question: 'What databases are supported?',
|
||||
question: "I'm a student. Can I use it for free?",
|
||||
answer:
|
||||
"Currently, data-peek is PostgreSQL-only. We're laser-focused on making the best Postgres experience possible. MySQL and SQLite support are planned for future releases.",
|
||||
'Absolutely! Students and educators can use data-peek for free, even for school projects. Just reach out on X (@gillarohith) or email gillarohith1@gmail.com and we\'ll hook you up with a free license. Learning should never have barriers.',
|
||||
},
|
||||
{
|
||||
question: 'How does the honor system work?',
|
||||
answer:
|
||||
'We trust you. There\'s no DRM, no aggressive license checks, no "you\'ve been logged out" surprises. If you\'re using it commercially, we ask that you pay for a license. Inspired by Yaak and other sustainable indie software.',
|
||||
},
|
||||
{
|
||||
question: 'How many devices can I use?',
|
||||
answer:
|
||||
'Each license includes 3 device activations. Use it on your work laptop, home computer, and one more device. Need more? Just reach out.',
|
||||
},
|
||||
{
|
||||
question: 'Is my data safe?',
|
||||
answer:
|
||||
'Yes. data-peek never sends your data anywhere. All queries run directly from your machine to your database. Connection credentials are encrypted locally. We have no telemetry, no analytics, no tracking.',
|
||||
'Yes. data-peek runs entirely on your machine. All queries go directly to your database — we never see your data. Connection credentials are encrypted locally. No telemetry, no analytics, no tracking.',
|
||||
},
|
||||
{
|
||||
question: 'What databases are supported?',
|
||||
answer:
|
||||
'Currently PostgreSQL and MySQL. We\'re laser-focused on making the best database experience possible. SQLite and more databases are planned for future releases.',
|
||||
},
|
||||
{
|
||||
question: 'Can I get a refund?',
|
||||
answer:
|
||||
"Yes, we offer a 14-day money-back guarantee. If data-peek isn't right for you, just email us and we'll refund your purchase, no questions asked.",
|
||||
},
|
||||
{
|
||||
question: 'Do you offer team or enterprise licenses?',
|
||||
answer:
|
||||
"Not yet, but we're working on it! Cloud sync and team features are coming soon. Sign up for our newsletter to be notified when they launch.",
|
||||
'Yes, 30-day money-back guarantee, no questions asked. If data-peek isn\'t right for you, just email us.',
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function Footer() {
|
|||
<Github className="w-4 h-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href="https://twitter.com/datapeekapp"
|
||||
href="https://x.com/gillarohith"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-9 h-9 rounded-lg bg-[--color-surface] border border-[--color-border] flex items-center justify-center text-[--color-text-muted] hover:text-[--color-text-primary] hover:border-[--color-text-muted] transition-colors"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ArrowRight, Sparkles, Zap, Download } from 'lucide-react'
|
||||
import { ArrowRight, Github, Zap, Download, Sparkles } from 'lucide-react'
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
|
|
@ -28,12 +28,16 @@ export function Hero() {
|
|||
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-6 pt-32 pb-20">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
{/* Early Bird Badge */}
|
||||
<div className="animate-fade-in-up">
|
||||
<Badge variant="default" size="lg" className="mb-8">
|
||||
{/* Early Bird + Open Source Badge */}
|
||||
<div className="animate-fade-in-up flex flex-wrap items-center justify-center gap-3 mb-8">
|
||||
<Badge variant="default" size="lg">
|
||||
<Sparkles className="w-3.5 h-3.5 mr-1.5" />
|
||||
Early Bird — 70% off
|
||||
</Badge>
|
||||
<Badge variant="secondary" size="lg">
|
||||
<Github className="w-3.5 h-3.5 mr-1.5" />
|
||||
Open Source
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Main Headline */}
|
||||
|
|
@ -52,7 +56,7 @@ export function Hero() {
|
|||
style={{ fontFamily: 'var(--font-body)' }}
|
||||
>
|
||||
A lightning-fast PostgreSQL client for developers who value simplicity.
|
||||
No bloat, no subscriptions, no BS.
|
||||
Open source, free for personal use.
|
||||
</p>
|
||||
|
||||
{/* Terminal-style feature highlight */}
|
||||
|
|
@ -70,7 +74,7 @@ export function Hero() {
|
|||
</span>
|
||||
<span className="w-px h-4 bg-[--color-border]" />
|
||||
<span className="flex items-center gap-2 text-sm">
|
||||
<span className="text-[--color-text-muted]">pay once, own forever</span>
|
||||
<span className="text-[--color-text-muted]">all features free</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Check, X, Sparkles, ArrowRight } from 'lucide-react'
|
||||
import { Check, Heart, Github, ArrowRight, Sparkles } from 'lucide-react'
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: 'Free',
|
||||
name: 'Personal',
|
||||
price: '$0',
|
||||
period: 'forever',
|
||||
description: 'Perfect for trying out data-peek',
|
||||
description: 'All features, personal use only',
|
||||
features: [
|
||||
{ text: '2 database connections', included: true },
|
||||
{ text: '50 query history items', included: true },
|
||||
{ text: '3 editor tabs', included: true },
|
||||
{ text: '1 schema for ER diagrams', included: true },
|
||||
{ text: 'CSV/JSON export', included: true },
|
||||
{ text: 'Inline data editing', included: false },
|
||||
{ text: 'Query execution plans', included: false },
|
||||
{ text: 'Updates', included: false },
|
||||
{ text: 'All features unlocked', included: true },
|
||||
{ text: 'Unlimited connections', included: true },
|
||||
{ text: 'Unlimited query history', included: true },
|
||||
{ text: 'All future updates', included: true },
|
||||
{ text: 'Personal projects & learning', included: true },
|
||||
{ text: 'Open source contributors', included: true },
|
||||
{ text: 'Students & educators', included: true },
|
||||
],
|
||||
cta: 'Download Free',
|
||||
href: '/download',
|
||||
|
|
@ -27,19 +26,17 @@ const plans = [
|
|||
name: 'Pro',
|
||||
price: '$29',
|
||||
originalPrice: '$99',
|
||||
period: 'one-time',
|
||||
description: 'For developers who need the full power',
|
||||
period: 'year',
|
||||
description: 'For commercial use at work',
|
||||
badge: 'Early Bird — 70% off',
|
||||
features: [
|
||||
{ text: 'Unlimited connections', included: true },
|
||||
{ text: 'Unlimited query history', included: true },
|
||||
{ text: 'Unlimited tabs', included: true },
|
||||
{ text: 'Unlimited ER diagrams', included: true },
|
||||
{ text: 'CSV/JSON export', included: true },
|
||||
{ text: 'Inline data editing', included: true },
|
||||
{ text: 'Query execution plans', included: true },
|
||||
{ text: '1 year of updates', included: true },
|
||||
{ text: 'Everything in Personal', included: true },
|
||||
{ text: 'Commercial use allowed', included: true },
|
||||
{ text: 'Use at work & for clients', included: true },
|
||||
{ text: '1 year of updates included', included: true },
|
||||
{ text: '3 device activations', included: true },
|
||||
{ text: 'Perpetual fallback license', included: true },
|
||||
{ text: '30-day money-back guarantee', included: true },
|
||||
],
|
||||
cta: 'Get Pro License',
|
||||
href: '#buy',
|
||||
|
|
@ -66,15 +63,16 @@ export function Pricing() {
|
|||
className="text-4xl md:text-5xl lg:text-6xl font-semibold tracking-tight mb-6"
|
||||
style={{ fontFamily: 'var(--font-display)' }}
|
||||
>
|
||||
Pay once.
|
||||
Open source.
|
||||
<br />
|
||||
<span className="text-[--color-text-secondary]">Own forever.</span>
|
||||
<span className="text-[--color-text-secondary]">Pay for commercial use.</span>
|
||||
</h2>
|
||||
<p
|
||||
className="text-lg text-[--color-text-secondary] max-w-xl mx-auto"
|
||||
style={{ fontFamily: 'var(--font-body)' }}
|
||||
>
|
||||
No subscriptions, no recurring fees. One purchase, lifetime access.
|
||||
Free for personal use. A license supports development and is required
|
||||
for commercial use in for-profit organizations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -92,7 +90,7 @@ export function Pricing() {
|
|||
{/* Badge */}
|
||||
{plan.badge && (
|
||||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||||
<Badge variant="default" size="md">
|
||||
<Badge variant="default" size="md" className="whitespace-nowrap">
|
||||
<Sparkles className="w-3 h-3 mr-1" />
|
||||
{plan.badge}
|
||||
</Badge>
|
||||
|
|
@ -128,22 +126,10 @@ export function Pricing() {
|
|||
<ul className="space-y-3 mb-8">
|
||||
{plan.features.map((feature) => (
|
||||
<li key={feature.text} className="flex items-center gap-3">
|
||||
{feature.included ? (
|
||||
<div className="w-5 h-5 rounded-full bg-[--color-success]/10 flex items-center justify-center flex-shrink-0">
|
||||
<Check className="w-3 h-3 text-[--color-success]" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full bg-[--color-text-muted]/10 flex items-center justify-center flex-shrink-0">
|
||||
<X className="w-3 h-3 text-[--color-text-muted]" />
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={`text-sm ${
|
||||
feature.included
|
||||
? 'text-[--color-text-primary]'
|
||||
: 'text-[--color-text-muted]'
|
||||
}`}
|
||||
>
|
||||
<div className="w-5 h-5 rounded-full bg-[--color-success]/10 flex items-center justify-center flex-shrink-0">
|
||||
<Check className="w-3 h-3 text-[--color-success]" />
|
||||
</div>
|
||||
<span className="text-sm text-[--color-text-primary]">
|
||||
{feature.text}
|
||||
</span>
|
||||
</li>
|
||||
|
|
@ -166,20 +152,69 @@ export function Pricing() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* Cloud Teaser */}
|
||||
<div className="mt-12 text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[--color-surface] border border-[--color-border]">
|
||||
<span
|
||||
className="text-sm text-[--color-text-muted]"
|
||||
style={{ fontFamily: 'var(--font-mono)' }}
|
||||
>
|
||||
Coming soon:
|
||||
</span>
|
||||
<span className="text-sm text-[--color-text-secondary]">
|
||||
Cloud sync & team features
|
||||
</span>
|
||||
{/* Honor System Notice */}
|
||||
<div className="mt-12 p-6 rounded-xl bg-[--color-surface] border border-[--color-border] max-w-2xl mx-auto">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-lg bg-[--color-accent]/10 flex items-center justify-center flex-shrink-0">
|
||||
<Heart className="w-5 h-5 text-[--color-accent]" />
|
||||
</div>
|
||||
<div>
|
||||
<h4
|
||||
className="text-base font-medium mb-2"
|
||||
style={{ fontFamily: 'var(--font-display)' }}
|
||||
>
|
||||
Honor System Licensing
|
||||
</h4>
|
||||
<p className="text-sm text-[--color-text-secondary] mb-3">
|
||||
Inspired by{' '}
|
||||
<Link
|
||||
href="https://yaak.app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[--color-accent] hover:underline"
|
||||
>
|
||||
Yaak
|
||||
</Link>{' '}
|
||||
and other sustainable indie software. No DRM, no aggressive enforcement.
|
||||
We trust you to do the right thing.
|
||||
</p>
|
||||
<p className="text-sm text-[--color-text-secondary]">
|
||||
<strong>Students:</strong> Use it free! Just reach out for a free license.
|
||||
</p>
|
||||
<p className="text-sm text-[--color-text-muted] mt-2">
|
||||
Questions?{' '}
|
||||
<Link
|
||||
href="https://x.com/gillarohith"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[--color-accent] hover:underline"
|
||||
>
|
||||
@gillarohith
|
||||
</Link>{' '}
|
||||
or{' '}
|
||||
<Link
|
||||
href="mailto:gillarohith1@gmail.com"
|
||||
className="text-[--color-accent] hover:underline"
|
||||
>
|
||||
gillarohith1@gmail.com
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Open Source Notice */}
|
||||
<div className="mt-6 text-center">
|
||||
<Link
|
||||
href="https://github.com/Rohithgilla12/data-peek"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-[--color-surface] border border-[--color-border] text-sm text-[--color-text-secondary] hover:text-[--color-text-primary] hover:border-[--color-text-muted] transition-colors"
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
<span>View source on GitHub — MIT Licensed</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,570 +0,0 @@
|
|||
# Table Designer Feature - Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
This plan outlines the implementation of a comprehensive Table Designer feature for data-peek, enabling users to create new tables and modify existing tables through a dedicated tab interface with full PostgreSQL feature support.
|
||||
|
||||
## Scope
|
||||
|
||||
- **Create Table**: Full-featured table creation with columns, constraints, indexes
|
||||
- **Edit Table (ALTER TABLE)**: Add/drop/rename columns, modify types, manage constraints and indexes
|
||||
- **PostgreSQL Full Features**: Sequences, partitioning, inheritance, custom types, CHECK constraints, UNIQUE constraints
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Shared Types & Data Models
|
||||
|
||||
### File: `packages/shared/src/index.ts`
|
||||
|
||||
Add new types for DDL operations:
|
||||
|
||||
```typescript
|
||||
// PostgreSQL data types for dropdown
|
||||
export type PostgresDataType =
|
||||
| 'smallint' | 'integer' | 'bigint' | 'serial' | 'bigserial'
|
||||
| 'numeric' | 'real' | 'double precision' | 'money'
|
||||
| 'char' | 'varchar' | 'text'
|
||||
| 'bytea'
|
||||
| 'timestamp' | 'timestamptz' | 'date' | 'time' | 'timetz' | 'interval'
|
||||
| 'boolean'
|
||||
| 'uuid'
|
||||
| 'json' | 'jsonb'
|
||||
| 'xml'
|
||||
| 'point' | 'line' | 'lseg' | 'box' | 'path' | 'polygon' | 'circle'
|
||||
| 'cidr' | 'inet' | 'macaddr'
|
||||
| 'int4range' | 'int8range' | 'numrange' | 'tsrange' | 'tstzrange' | 'daterange'
|
||||
| 'custom' // For custom/enum types
|
||||
|
||||
// Column definition for table designer
|
||||
export interface ColumnDefinition {
|
||||
id: string // Client-side tracking
|
||||
name: string
|
||||
dataType: PostgresDataType | string
|
||||
length?: number // For varchar(n), char(n)
|
||||
precision?: number // For numeric(p,s)
|
||||
scale?: number
|
||||
isNullable: boolean
|
||||
isPrimaryKey: boolean
|
||||
isUnique: boolean
|
||||
defaultValue?: string
|
||||
defaultType?: 'value' | 'expression' | 'sequence'
|
||||
sequenceName?: string // For nextval('sequence')
|
||||
checkConstraint?: string
|
||||
comment?: string
|
||||
collation?: string
|
||||
isArray?: boolean // For array types like text[]
|
||||
}
|
||||
|
||||
// Constraint types
|
||||
export type ConstraintType = 'primary_key' | 'foreign_key' | 'unique' | 'check' | 'exclude'
|
||||
|
||||
export interface ConstraintDefinition {
|
||||
id: string
|
||||
name?: string // Optional, auto-generated if not provided
|
||||
type: ConstraintType
|
||||
columns: string[]
|
||||
// Foreign key specific
|
||||
referencedSchema?: string
|
||||
referencedTable?: string
|
||||
referencedColumns?: string[]
|
||||
onUpdate?: 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT'
|
||||
onDelete?: 'NO ACTION' | 'RESTRICT' | 'CASCADE' | 'SET NULL' | 'SET DEFAULT'
|
||||
// Check constraint specific
|
||||
checkExpression?: string
|
||||
// Exclude constraint specific (advanced)
|
||||
excludeElements?: Array<{ column: string; operator: string }>
|
||||
excludeUsing?: 'btree' | 'gist' | 'gin' | 'hash'
|
||||
}
|
||||
|
||||
// Index definition
|
||||
export interface IndexDefinition {
|
||||
id: string
|
||||
name?: string
|
||||
columns: Array<{
|
||||
name: string
|
||||
order?: 'ASC' | 'DESC'
|
||||
nullsPosition?: 'FIRST' | 'LAST'
|
||||
}>
|
||||
isUnique: boolean
|
||||
method?: 'btree' | 'hash' | 'gist' | 'gin' | 'spgist' | 'brin'
|
||||
where?: string // Partial index condition
|
||||
include?: string[] // INCLUDE columns (covering index)
|
||||
concurrent?: boolean
|
||||
}
|
||||
|
||||
// Table partitioning
|
||||
export interface PartitionDefinition {
|
||||
type: 'RANGE' | 'LIST' | 'HASH'
|
||||
columns: string[]
|
||||
}
|
||||
|
||||
// Full table definition
|
||||
export interface TableDefinition {
|
||||
schema: string
|
||||
name: string
|
||||
columns: ColumnDefinition[]
|
||||
constraints: ConstraintDefinition[]
|
||||
indexes: IndexDefinition[]
|
||||
partition?: PartitionDefinition
|
||||
inherits?: string[] // Parent tables
|
||||
tablespace?: string
|
||||
comment?: string
|
||||
withOids?: boolean
|
||||
unlogged?: boolean
|
||||
}
|
||||
|
||||
// ALTER TABLE operations
|
||||
export type AlterColumnOperation =
|
||||
| { type: 'add'; column: ColumnDefinition }
|
||||
| { type: 'drop'; columnName: string; cascade?: boolean }
|
||||
| { type: 'rename'; oldName: string; newName: string }
|
||||
| { type: 'set_type'; columnName: string; newType: string; using?: string }
|
||||
| { type: 'set_nullable'; columnName: string; nullable: boolean }
|
||||
| { type: 'set_default'; columnName: string; defaultValue: string | null }
|
||||
| { type: 'set_comment'; columnName: string; comment: string | null }
|
||||
|
||||
export type AlterConstraintOperation =
|
||||
| { type: 'add_constraint'; constraint: ConstraintDefinition }
|
||||
| { type: 'drop_constraint'; name: string; cascade?: boolean }
|
||||
| { type: 'rename_constraint'; oldName: string; newName: string }
|
||||
|
||||
export type AlterIndexOperation =
|
||||
| { type: 'create_index'; index: IndexDefinition }
|
||||
| { type: 'drop_index'; name: string; cascade?: boolean; concurrent?: boolean }
|
||||
| { type: 'rename_index'; oldName: string; newName: string }
|
||||
| { type: 'reindex'; name: string; concurrent?: boolean }
|
||||
|
||||
export interface AlterTableBatch {
|
||||
schema: string
|
||||
table: string
|
||||
columnOperations: AlterColumnOperation[]
|
||||
constraintOperations: AlterConstraintOperation[]
|
||||
indexOperations: AlterIndexOperation[]
|
||||
renameTable?: string
|
||||
setSchema?: string
|
||||
comment?: string | null
|
||||
}
|
||||
|
||||
// Result types
|
||||
export interface DDLResult {
|
||||
success: boolean
|
||||
executedSql: string[]
|
||||
errors?: string[]
|
||||
}
|
||||
|
||||
// Metadata for UI
|
||||
export interface SequenceInfo {
|
||||
schema: string
|
||||
name: string
|
||||
dataType: string
|
||||
startValue: string
|
||||
increment: string
|
||||
}
|
||||
|
||||
export interface CustomTypeInfo {
|
||||
schema: string
|
||||
name: string
|
||||
type: 'enum' | 'composite' | 'range' | 'domain'
|
||||
values?: string[] // For enums
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: DDL SQL Builder
|
||||
|
||||
### New File: `apps/desktop/src/main/ddl-builder.ts`
|
||||
|
||||
Create a dedicated builder for DDL statements:
|
||||
|
||||
```typescript
|
||||
// Key functions to implement:
|
||||
export function buildCreateTable(definition: TableDefinition): ParameterizedQuery
|
||||
export function buildAlterTable(batch: AlterTableBatch): ParameterizedQuery[]
|
||||
export function buildDropTable(schema: string, table: string, cascade?: boolean): ParameterizedQuery
|
||||
export function buildCreateIndex(schema: string, table: string, index: IndexDefinition): ParameterizedQuery
|
||||
export function buildPreviewDDL(definition: TableDefinition): string // For UI preview
|
||||
export function buildAlterPreviewDDL(batch: AlterTableBatch): string[]
|
||||
```
|
||||
|
||||
Key considerations:
|
||||
- Quote all identifiers properly
|
||||
- Handle reserved words
|
||||
- Escape string values in defaults
|
||||
- Support all PostgreSQL constraint syntax
|
||||
- Handle partial indexes with WHERE
|
||||
- Support expression indexes
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: IPC Handlers
|
||||
|
||||
### File: `apps/desktop/src/main/index.ts`
|
||||
|
||||
Add new IPC handlers:
|
||||
|
||||
```typescript
|
||||
// Create table
|
||||
ipcMain.handle('db:create-table', async (_, { config, definition }) => {
|
||||
// Execute CREATE TABLE statement
|
||||
// Return DDLResult
|
||||
})
|
||||
|
||||
// Alter table
|
||||
ipcMain.handle('db:alter-table', async (_, { config, batch }) => {
|
||||
// Execute ALTER TABLE statements in transaction
|
||||
// Return DDLResult
|
||||
})
|
||||
|
||||
// Drop table
|
||||
ipcMain.handle('db:drop-table', async (_, { config, schema, table, cascade }) => {
|
||||
// Execute DROP TABLE
|
||||
// Return DDLResult
|
||||
})
|
||||
|
||||
// Get table DDL (reverse engineer)
|
||||
ipcMain.handle('db:get-table-ddl', async (_, { config, schema, table }) => {
|
||||
// Query pg_catalog to reconstruct CREATE TABLE
|
||||
// Return TableDefinition
|
||||
})
|
||||
|
||||
// Get available sequences
|
||||
ipcMain.handle('db:get-sequences', async (_, config) => {
|
||||
// Query pg_sequences
|
||||
// Return SequenceInfo[]
|
||||
})
|
||||
|
||||
// Get custom types
|
||||
ipcMain.handle('db:get-types', async (_, config) => {
|
||||
// Query pg_type for enums, composites, etc.
|
||||
// Return CustomTypeInfo[]
|
||||
})
|
||||
|
||||
// Preview DDL without executing
|
||||
ipcMain.handle('db:preview-ddl', async (_, { definition }) => {
|
||||
// Return generated SQL string
|
||||
})
|
||||
```
|
||||
|
||||
### File: `apps/desktop/src/preload/index.ts`
|
||||
|
||||
Update API exposure:
|
||||
|
||||
```typescript
|
||||
const api = {
|
||||
// ... existing
|
||||
ddl: {
|
||||
createTable: (config, definition) => ipcRenderer.invoke('db:create-table', { config, definition }),
|
||||
alterTable: (config, batch) => ipcRenderer.invoke('db:alter-table', { config, batch }),
|
||||
dropTable: (config, schema, table, cascade) =>
|
||||
ipcRenderer.invoke('db:drop-table', { config, schema, table, cascade }),
|
||||
getTableDDL: (config, schema, table) =>
|
||||
ipcRenderer.invoke('db:get-table-ddl', { config, schema, table }),
|
||||
getSequences: (config) => ipcRenderer.invoke('db:get-sequences', config),
|
||||
getTypes: (config) => ipcRenderer.invoke('db:get-types', config),
|
||||
previewDDL: (definition) => ipcRenderer.invoke('db:preview-ddl', { definition })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Tab Store Extension
|
||||
|
||||
### File: `apps/desktop/src/renderer/src/stores/tab-store.ts`
|
||||
|
||||
Add new tab type:
|
||||
|
||||
```typescript
|
||||
export type TabType = 'query' | 'table-preview' | 'erd' | 'table-designer'
|
||||
|
||||
export interface TableDesignerTab extends BaseTab {
|
||||
type: 'table-designer'
|
||||
schemaName: string
|
||||
tableName?: string // undefined for new table
|
||||
mode: 'create' | 'edit'
|
||||
isDirty: boolean
|
||||
}
|
||||
|
||||
// New actions
|
||||
createTableDesignerTab: (connectionId: string, schemaName: string, tableName?: string) => string
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: DDL Store
|
||||
|
||||
### New File: `apps/desktop/src/renderer/src/stores/ddl-store.ts`
|
||||
|
||||
Create Zustand store for table designer state:
|
||||
|
||||
```typescript
|
||||
interface DDLState {
|
||||
// Per-tab state (keyed by tabId)
|
||||
tabState: Map<string, TableDesignerState>
|
||||
|
||||
// Actions
|
||||
initializeTab: (tabId: string, mode: 'create' | 'edit', definition?: TableDefinition) => void
|
||||
|
||||
// Column operations
|
||||
addColumn: (tabId: string, column?: Partial<ColumnDefinition>) => void
|
||||
updateColumn: (tabId: string, columnId: string, updates: Partial<ColumnDefinition>) => void
|
||||
removeColumn: (tabId: string, columnId: string) => void
|
||||
reorderColumns: (tabId: string, startIndex: number, endIndex: number) => void
|
||||
|
||||
// Constraint operations
|
||||
addConstraint: (tabId: string, constraint: ConstraintDefinition) => void
|
||||
updateConstraint: (tabId: string, constraintId: string, updates: Partial<ConstraintDefinition>) => void
|
||||
removeConstraint: (tabId: string, constraintId: string) => void
|
||||
|
||||
// Index operations
|
||||
addIndex: (tabId: string, index: IndexDefinition) => void
|
||||
updateIndex: (tabId: string, indexId: string, updates: Partial<IndexDefinition>) => void
|
||||
removeIndex: (tabId: string, indexId: string) => void
|
||||
|
||||
// Table properties
|
||||
updateTableProperties: (tabId: string, updates: Partial<TableDefinition>) => void
|
||||
|
||||
// Diff tracking for edit mode
|
||||
getAlterTableBatch: (tabId: string) => AlterTableBatch | null
|
||||
|
||||
// Validation
|
||||
validateDefinition: (tabId: string) => { valid: boolean; errors: string[] }
|
||||
|
||||
// Cleanup
|
||||
clearTabState: (tabId: string) => void
|
||||
}
|
||||
|
||||
interface TableDesignerState {
|
||||
original?: TableDefinition // For edit mode - track original state
|
||||
current: TableDefinition
|
||||
isDirty: boolean
|
||||
validationErrors: string[]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: UI Components
|
||||
|
||||
### New Components Structure:
|
||||
|
||||
```
|
||||
src/renderer/src/components/
|
||||
├── table-designer/
|
||||
│ ├── table-designer.tsx # Main container
|
||||
│ ├── column-list.tsx # Column grid with drag-drop
|
||||
│ ├── column-editor-row.tsx # Inline column editing
|
||||
│ ├── column-editor-dialog.tsx # Full column editor modal
|
||||
│ ├── constraint-list.tsx # Constraint management
|
||||
│ ├── constraint-editor-dialog.tsx # Add/edit constraint
|
||||
│ ├── index-list.tsx # Index management
|
||||
│ ├── index-editor-dialog.tsx # Add/edit index
|
||||
│ ├── table-properties.tsx # Table-level settings
|
||||
│ ├── ddl-preview-panel.tsx # Live SQL preview
|
||||
│ ├── data-type-select.tsx # Type picker with search
|
||||
│ └── sequence-select.tsx # Sequence picker
|
||||
```
|
||||
|
||||
### Main Component: `table-designer.tsx`
|
||||
|
||||
Layout:
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Toolbar: [Save] [Preview SQL] [Cancel] | Table: schema.name │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ Tabs: [Columns] [Constraints] [Indexes] [Properties] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ Column Grid (primary view) │ │
|
||||
│ │ ┌─────┬──────────┬──────────┬────┬────┬────┬─────────┐ │ │
|
||||
│ │ │ ⋮⋮ │ Name │ Type │ PK │ NN │ UQ │ Default │ │ │
|
||||
│ │ ├─────┼──────────┼──────────┼────┼────┼────┼─────────┤ │ │
|
||||
│ │ │ ⋮⋮ │ id │ uuid │ ✓ │ ✓ │ │ gen_... │ │ │
|
||||
│ │ │ ⋮⋮ │ name │ varchar │ │ ✓ │ │ │ │ │
|
||||
│ │ │ ⋮⋮ │ email │ varchar │ │ ✓ │ ✓ │ │ │ │
|
||||
│ │ │ ⋮⋮ │ created │ timestamp│ │ ✓ │ │ now() │ │ │
|
||||
│ │ └─────┴──────────┴──────────┴────┴────┴────┴─────────┘ │ │
|
||||
│ │ [+ Add Column] │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────────────────────┐ │
|
||||
│ │ DDL Preview (collapsible) │ │
|
||||
│ │ CREATE TABLE schema.table ( │ │
|
||||
│ │ id uuid PRIMARY KEY DEFAULT gen_random_uuid(), │ │
|
||||
│ │ ... │ │
|
||||
│ │ ); │ │
|
||||
│ └──────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Features by Tab:
|
||||
|
||||
**Columns Tab:**
|
||||
- Drag-drop reordering
|
||||
- Inline editing for quick changes
|
||||
- Click to expand for advanced options
|
||||
- Visual indicators: PK (key icon), NOT NULL (*), UNIQUE (U)
|
||||
- Type suggestions as you type
|
||||
- Array type toggle
|
||||
- Collation selector for text types
|
||||
|
||||
**Constraints Tab:**
|
||||
- List view with constraint type badges
|
||||
- Add constraint button with type selector
|
||||
- Foreign key builder with table/column pickers
|
||||
- Check constraint with SQL editor
|
||||
- Unique constraint with column multi-select
|
||||
|
||||
**Indexes Tab:**
|
||||
- List of indexes with method badges
|
||||
- Column selector with order (ASC/DESC)
|
||||
- Partial index condition editor
|
||||
- INCLUDE column selector
|
||||
- CONCURRENT option toggle
|
||||
|
||||
**Properties Tab:**
|
||||
- Table comment
|
||||
- Schema selector (for changing schema)
|
||||
- Tablespace selector
|
||||
- Partition configuration
|
||||
- Inheritance configuration
|
||||
- UNLOGGED toggle
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: Integration Points
|
||||
|
||||
### Schema Explorer Updates
|
||||
|
||||
Add to `schema-explorer.tsx`:
|
||||
|
||||
1. "Create Table" button in schema header:
|
||||
```tsx
|
||||
<Button onClick={() => createTableDesignerTab(connectionId, schema.name)}>
|
||||
<Plus className="size-3.5" />
|
||||
</Button>
|
||||
```
|
||||
|
||||
2. Context menu on tables:
|
||||
```tsx
|
||||
<ContextMenu>
|
||||
<ContextMenuItem onClick={() => createTableDesignerTab(connectionId, schema.name, table.name)}>
|
||||
Edit Table Structure
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => handleDropTable(schema.name, table.name)}>
|
||||
Drop Table...
|
||||
</ContextMenuItem>
|
||||
</ContextMenu>
|
||||
```
|
||||
|
||||
### Tab Container Updates
|
||||
|
||||
Add to `tab-container.tsx`:
|
||||
|
||||
```tsx
|
||||
case 'table-designer':
|
||||
return <TableDesigner tabId={tab.id} />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 8: Implementation Order
|
||||
|
||||
### Step 1: Foundation (Day 1-2)
|
||||
1. Add all shared types to `packages/shared/src/index.ts`
|
||||
2. Create `ddl-builder.ts` with CREATE TABLE generation
|
||||
3. Add basic IPC handlers for create-table and preview-ddl
|
||||
|
||||
### Step 2: Basic UI (Day 2-3)
|
||||
1. Create DDL store
|
||||
2. Add new tab type to tab-store
|
||||
3. Create basic TableDesigner component with column list
|
||||
4. Add column editing (inline + dialog)
|
||||
5. Add DDL preview panel
|
||||
|
||||
### Step 3: Column Features (Day 3-4)
|
||||
1. Implement all column properties
|
||||
2. Add data type selector with all PostgreSQL types
|
||||
3. Add sequence picker for defaults
|
||||
4. Implement column validation
|
||||
|
||||
### Step 4: Constraints (Day 4-5)
|
||||
1. Add constraint list UI
|
||||
2. Implement PRIMARY KEY constraint
|
||||
3. Implement FOREIGN KEY with table/column picker
|
||||
4. Implement UNIQUE and CHECK constraints
|
||||
|
||||
### Step 5: Indexes (Day 5)
|
||||
1. Add index list UI
|
||||
2. Implement index creation with all options
|
||||
3. Add partial index support
|
||||
|
||||
### Step 6: ALTER TABLE (Day 6-7)
|
||||
1. Implement `db:get-table-ddl` handler (reverse engineer table)
|
||||
2. Implement diff tracking in DDL store
|
||||
3. Generate ALTER TABLE statements
|
||||
4. Handle column type changes with USING
|
||||
|
||||
### Step 7: Advanced Features (Day 7-8)
|
||||
1. Add partitioning support
|
||||
2. Add inheritance support
|
||||
3. Add custom type support
|
||||
4. Add schema explorer integration (create button, context menu)
|
||||
|
||||
### Step 8: Polish (Day 8)
|
||||
1. Validation and error handling
|
||||
2. Keyboard shortcuts
|
||||
3. Undo/redo support
|
||||
4. Test with various edge cases
|
||||
|
||||
---
|
||||
|
||||
## Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Cmd+S` | Save/Execute DDL |
|
||||
| `Cmd+Enter` | Preview SQL |
|
||||
| `Cmd+N` | Add new column |
|
||||
| `Delete` | Remove selected column |
|
||||
| `Cmd+Z` | Undo last change |
|
||||
| `Cmd+Shift+Z` | Redo |
|
||||
| `Tab` | Move to next column field |
|
||||
| `Shift+Tab` | Move to previous field |
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
1. **Table name**: Required, valid identifier, not reserved word
|
||||
2. **Column names**: Required, unique within table, valid identifier
|
||||
3. **Primary key**: At most one (can be composite)
|
||||
4. **Foreign keys**: Referenced table must exist, column types must match
|
||||
5. **Check constraints**: Valid SQL expression
|
||||
6. **Default values**: Type-compatible with column
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
1. **Pre-execution validation**: Show errors inline before attempting execution
|
||||
2. **Execution errors**: Display PostgreSQL error message clearly
|
||||
3. **Partial success**: If ALTER TABLE fails mid-batch, show which operations succeeded
|
||||
4. **Recovery**: Provide option to rollback or continue
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
1. Create simple table with various column types
|
||||
2. Create table with composite primary key
|
||||
3. Create table with foreign keys to multiple tables
|
||||
4. Create table with CHECK constraints
|
||||
5. Create table with partial indexes
|
||||
6. Edit existing table - add column
|
||||
7. Edit existing table - change column type
|
||||
8. Edit existing table - add/remove constraints
|
||||
9. Drop table with cascade
|
||||
10. Create table with partitioning
|
||||
|
|
@ -1,511 +0,0 @@
|
|||
# data-peek Backend & Business Plan
|
||||
|
||||
> Planning document for the data-peek licensing system, payment integration, and marketing site.
|
||||
|
||||
---
|
||||
|
||||
## Business Model
|
||||
|
||||
### Pricing Strategy
|
||||
|
||||
| Tier | Price | What's Included |
|
||||
|------|-------|-----------------|
|
||||
| **Pro License** | ~~$99~~ **$29** (Early Bird) | Perpetual license, 1 year of updates, 3 device activations |
|
||||
| **Free Tier** | $0 | Limited features (see below) |
|
||||
| **Cloud** (Future) | ~$5-8/month | Connection sync, saved queries, team features |
|
||||
|
||||
### Early Bird Promotion
|
||||
- **Regular Price:** $99
|
||||
- **Launch Price:** $29 (Dec 2024 / Early Adopters)
|
||||
- **Messaging:** "70% off for early supporters"
|
||||
|
||||
### Free vs Pro Features
|
||||
|
||||
| Feature | Free | Pro ($29) |
|
||||
|---------|------|-----------|
|
||||
| Connections | 2 | Unlimited |
|
||||
| Query History | 50 queries | Unlimited |
|
||||
| Editor Tabs | 3 | Unlimited |
|
||||
| Export CSV/JSON | Yes | Yes |
|
||||
| ER Diagrams | 1 schema | Unlimited |
|
||||
| Inline Editing | View only | Full CRUD |
|
||||
| Query Execution Plans | No | Yes |
|
||||
| Updates | No | 1 year |
|
||||
| Device Activations | 1 | 3 |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Technology | Why |
|
||||
|-----------|------------|-----|
|
||||
| Marketing Site | Next.js 14 (App Router) | SSR, API routes, great DX |
|
||||
| Database | PostgreSQL (Supabase or Neon) | Familiar, reliable |
|
||||
| Auth | NextAuth.js | Easy OAuth + credentials |
|
||||
| Payments | DodoPayments | MoR, global reach, one-time + subscriptions |
|
||||
| Email | Resend | Developer-friendly, good deliverability |
|
||||
| Hosting | Vercel | Seamless Next.js deployment |
|
||||
| File Storage | Cloudflare R2 or S3 | App binary downloads |
|
||||
| Analytics | Plausible or PostHog | Privacy-friendly |
|
||||
|
||||
---
|
||||
|
||||
## DodoPayments Integration
|
||||
|
||||
### Why DodoPayments
|
||||
- Merchant of Record (handles taxes, VAT globally)
|
||||
- 150+ countries, 80+ currencies
|
||||
- One-time payments support (perfect for perpetual licenses)
|
||||
- SDKs for TypeScript/Node.js
|
||||
- Webhook support for automation
|
||||
|
||||
### Product Setup in DodoPayments
|
||||
```
|
||||
Product: data-peek Pro License
|
||||
Type: One-time payment
|
||||
Price: $29 (promo) / $99 (regular)
|
||||
Metadata:
|
||||
- license_type: "pro"
|
||||
- updates_duration: "1_year"
|
||||
- max_activations: 3
|
||||
```
|
||||
|
||||
### Webhook Events to Handle
|
||||
| Event | Action |
|
||||
|-------|--------|
|
||||
| `payment.completed` | Generate license key, store in DB, send email |
|
||||
| `payment.refunded` | Revoke license, update status |
|
||||
| `payment.failed` | Log for debugging, notify if needed |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Tables
|
||||
|
||||
```sql
|
||||
-- Customers (synced from DodoPayments)
|
||||
CREATE TABLE customers (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
name TEXT,
|
||||
dodo_customer_id TEXT UNIQUE,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Licenses
|
||||
CREATE TABLE licenses (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
customer_id UUID REFERENCES customers(id),
|
||||
license_key TEXT UNIQUE NOT NULL,
|
||||
plan TEXT NOT NULL DEFAULT 'pro',
|
||||
status TEXT NOT NULL DEFAULT 'active', -- active, revoked, expired
|
||||
max_activations INT NOT NULL DEFAULT 3,
|
||||
dodo_payment_id TEXT UNIQUE,
|
||||
dodo_product_id TEXT,
|
||||
purchased_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updates_until TIMESTAMPTZ NOT NULL, -- purchased_at + 1 year
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Device Activations
|
||||
CREATE TABLE activations (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
license_id UUID REFERENCES licenses(id) ON DELETE CASCADE,
|
||||
device_id TEXT NOT NULL, -- Hardware fingerprint
|
||||
device_name TEXT, -- e.g., "MacBook Pro M2"
|
||||
os TEXT, -- macos, windows, linux
|
||||
app_version TEXT,
|
||||
activated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_validated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
UNIQUE(license_id, device_id)
|
||||
);
|
||||
|
||||
-- App Releases (for update checks)
|
||||
CREATE TABLE releases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
version TEXT UNIQUE NOT NULL, -- semver: 1.0.0
|
||||
release_notes TEXT,
|
||||
download_url_mac TEXT,
|
||||
download_url_mac_arm TEXT,
|
||||
download_url_windows TEXT,
|
||||
download_url_linux TEXT,
|
||||
is_latest BOOLEAN DEFAULT FALSE,
|
||||
min_supported_version TEXT, -- for forced updates
|
||||
released_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes
|
||||
CREATE INDEX idx_licenses_customer ON licenses(customer_id);
|
||||
CREATE INDEX idx_licenses_key ON licenses(license_key);
|
||||
CREATE INDEX idx_activations_license ON activations(license_id);
|
||||
CREATE INDEX idx_activations_device ON activations(device_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public API (called by Electron app)
|
||||
|
||||
```
|
||||
POST /api/license/validate
|
||||
Request: { licenseKey, deviceId }
|
||||
Response: { valid, plan, status, updatesUntil, activationsRemaining }
|
||||
|
||||
POST /api/license/activate
|
||||
Request: { licenseKey, deviceId, deviceName, os, appVersion }
|
||||
Response: { success, activation, license }
|
||||
|
||||
POST /api/license/deactivate
|
||||
Request: { licenseKey, deviceId }
|
||||
Response: { success, activationsRemaining }
|
||||
|
||||
GET /api/updates/check?version=1.0.0&platform=macos
|
||||
Response: { hasUpdate, latestVersion, downloadUrl, releaseNotes, forceUpdate }
|
||||
```
|
||||
|
||||
### Webhook Endpoint
|
||||
|
||||
```
|
||||
POST /api/webhooks/dodo
|
||||
- Verify signature from DodoPayments
|
||||
- Handle payment.completed → create license
|
||||
- Handle payment.refunded → revoke license
|
||||
```
|
||||
|
||||
### Protected API (requires auth, for account dashboard)
|
||||
|
||||
```
|
||||
GET /api/account/licenses # List user's licenses
|
||||
GET /api/account/activations # List active devices
|
||||
POST /api/account/deactivate # Deactivate a device remotely
|
||||
GET /api/account/downloads # Get download links
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License Key Generation
|
||||
|
||||
### Format
|
||||
```
|
||||
DPRO-XXXX-XXXX-XXXX-XXXX
|
||||
```
|
||||
- Prefix: `DPRO` (data-peek pro)
|
||||
- 4 groups of 4 alphanumeric characters
|
||||
- Total: 20 characters (easy to type, hard to guess)
|
||||
|
||||
### Generation Logic
|
||||
```typescript
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
function generateLicenseKey(): string {
|
||||
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' // No 0, O, 1, I (avoid confusion)
|
||||
const segments: string[] = []
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let segment = ''
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const randomIndex = randomBytes(1)[0] % chars.length
|
||||
segment += chars[randomIndex]
|
||||
}
|
||||
segments.push(segment)
|
||||
}
|
||||
|
||||
return `DPRO-${segments.join('-')}`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Electron App Integration
|
||||
|
||||
### License Activation Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ FIRST LAUNCH │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
|
||||
│ │ Show │ │ User enters │ │ Call /activate │ │
|
||||
│ │ License │───▶│ license key │───▶│ API endpoint │ │
|
||||
│ │ Dialog │ │ │ │ │ │
|
||||
│ └─────────────┘ └──────────────┘ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────┴──────┐ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────┐ ┌───────────┐ │
|
||||
│ │ Valid │ │ Invalid │ │
|
||||
│ │ License │ │ Show │ │
|
||||
│ │ Store │ │ Error │ │
|
||||
│ │ locally │ │ │ │
|
||||
│ └─────────────┘ └───────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Validation Strategy
|
||||
|
||||
```typescript
|
||||
// On app startup
|
||||
async function checkLicense(): Promise<LicenseStatus> {
|
||||
const stored = await getLicenseFromStore()
|
||||
|
||||
if (!stored) {
|
||||
return { status: 'free', features: FREE_FEATURES }
|
||||
}
|
||||
|
||||
// Try online validation
|
||||
try {
|
||||
const result = await validateOnline(stored.key, getDeviceId())
|
||||
|
||||
if (result.valid) {
|
||||
// Update local cache
|
||||
await updateLicenseCache(result)
|
||||
return { status: 'pro', features: PRO_FEATURES, license: result }
|
||||
} else {
|
||||
// License revoked or invalid
|
||||
await clearLicenseStore()
|
||||
return { status: 'free', features: FREE_FEATURES, error: result.reason }
|
||||
}
|
||||
} catch (error) {
|
||||
// Offline - use cached validation (grace period)
|
||||
if (stored.cachedValidation && isWithinGracePeriod(stored.lastValidated)) {
|
||||
return { status: 'pro', features: PRO_FEATURES, offline: true }
|
||||
}
|
||||
return { status: 'free', features: FREE_FEATURES }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Device Fingerprinting
|
||||
|
||||
```typescript
|
||||
import { machineIdSync } from 'node-machine-id'
|
||||
import os from 'os'
|
||||
|
||||
function getDeviceId(): string {
|
||||
return machineIdSync() // Unique per machine
|
||||
}
|
||||
|
||||
function getDeviceName(): string {
|
||||
return os.hostname()
|
||||
}
|
||||
|
||||
function getDeviceInfo() {
|
||||
return {
|
||||
deviceId: getDeviceId(),
|
||||
deviceName: getDeviceName(),
|
||||
os: process.platform, // darwin, win32, linux
|
||||
appVersion: app.getVersion()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Marketing Site Structure
|
||||
|
||||
### Pages
|
||||
|
||||
```
|
||||
/ # Landing page
|
||||
/pricing # Pricing with buy button
|
||||
/download # Platform-specific downloads
|
||||
/docs # Getting started guide
|
||||
/docs/[slug] # Individual doc pages
|
||||
/faq # Frequently asked questions
|
||||
/changelog # Release history
|
||||
/blog # Updates, tutorials (future)
|
||||
/account # Dashboard (protected)
|
||||
/account/licenses # Manage licenses (protected)
|
||||
/account/downloads # Download links (protected)
|
||||
/login # Auth page
|
||||
```
|
||||
|
||||
### Landing Page Sections
|
||||
|
||||
1. **Hero** - Tagline, screenshot, CTA button
|
||||
2. **Problem** - Pain points with existing tools
|
||||
3. **Features** - Key capabilities with visuals
|
||||
4. **Demo** - GIF or video of the app in action
|
||||
5. **Comparison** - vs pgAdmin, DBeaver, TablePlus
|
||||
6. **Pricing** - Single tier, early bird callout
|
||||
7. **FAQ** - Common questions
|
||||
8. **CTA** - Final call to action
|
||||
|
||||
---
|
||||
|
||||
## Email Templates
|
||||
|
||||
### Welcome Email (after purchase)
|
||||
```
|
||||
Subject: Your data-peek Pro license 🎉
|
||||
|
||||
Hi {name},
|
||||
|
||||
Thank you for purchasing data-peek Pro!
|
||||
|
||||
Your license key: {license_key}
|
||||
|
||||
Quick start:
|
||||
1. Download data-peek: {download_link}
|
||||
2. Open the app and go to Settings → License
|
||||
3. Enter your license key
|
||||
|
||||
Your license includes:
|
||||
✓ 1 year of updates (until {updates_until})
|
||||
✓ 3 device activations
|
||||
✓ All Pro features unlocked
|
||||
|
||||
Need help? Reply to this email.
|
||||
|
||||
Happy querying!
|
||||
— The data-peek team
|
||||
```
|
||||
|
||||
### License Activation Email
|
||||
```
|
||||
Subject: data-peek activated on {device_name}
|
||||
|
||||
Your license was just activated on a new device:
|
||||
|
||||
Device: {device_name}
|
||||
Activated: {timestamp}
|
||||
Activations used: {used}/{max}
|
||||
|
||||
Not you? Manage your devices at {dashboard_link}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Phase 1: MVP (Week 1)
|
||||
- [ ] Set up Next.js project with App Router
|
||||
- [ ] Configure DodoPayments account and product
|
||||
- [ ] Create database schema (Supabase/Neon)
|
||||
- [ ] Implement `/api/webhooks/dodo` endpoint
|
||||
- [ ] Implement `/api/license/validate` endpoint
|
||||
- [ ] Implement `/api/license/activate` endpoint
|
||||
- [ ] Build landing page with pricing
|
||||
- [ ] Build download page
|
||||
- [ ] Deploy to Vercel
|
||||
- [ ] Test end-to-end purchase flow
|
||||
|
||||
### Phase 2: Polish (Week 2)
|
||||
- [ ] Add account dashboard with NextAuth
|
||||
- [ ] Implement device management UI
|
||||
- [ ] Add email notifications (Resend)
|
||||
- [ ] Implement update checker API
|
||||
- [ ] Add analytics (Plausible)
|
||||
- [ ] Write FAQ page
|
||||
- [ ] Add basic docs/getting started
|
||||
|
||||
### Phase 3: Electron Integration (Week 2-3)
|
||||
- [ ] Add license dialog to data-peek app
|
||||
- [ ] Implement device fingerprinting
|
||||
- [ ] Add license validation on startup
|
||||
- [ ] Implement feature gating (free vs pro)
|
||||
- [ ] Add "Check for updates" functionality
|
||||
- [ ] Handle offline grace period
|
||||
- [ ] Test activation/deactivation flow
|
||||
|
||||
### Phase 4: Launch Prep (Week 3)
|
||||
- [ ] Create app store screenshots
|
||||
- [ ] Record demo video/GIF
|
||||
- [ ] Write launch blog post
|
||||
- [ ] Set up social media accounts
|
||||
- [ ] Prepare ProductHunt launch
|
||||
- [ ] Build releases for all platforms
|
||||
- [ ] Final QA testing
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### API Security
|
||||
- [ ] Rate limiting on all endpoints (prevent brute force)
|
||||
- [ ] Webhook signature verification (DodoPayments)
|
||||
- [ ] Input validation on all parameters
|
||||
- [ ] SQL injection prevention (parameterized queries)
|
||||
|
||||
### License Security
|
||||
- [ ] License keys stored hashed in DB? (or encrypted)
|
||||
- [ ] Device ID cannot be spoofed easily
|
||||
- [ ] Offline grace period limited (7-14 days)
|
||||
- [ ] Anomaly detection (too many activations)
|
||||
|
||||
### Data Privacy
|
||||
- [ ] Minimal data collection
|
||||
- [ ] No tracking in the app
|
||||
- [ ] Clear privacy policy
|
||||
- [ ] GDPR compliance (if EU customers)
|
||||
|
||||
---
|
||||
|
||||
## Metrics to Track
|
||||
|
||||
### Business Metrics
|
||||
- Total licenses sold
|
||||
- Revenue (MRR if cloud tier added)
|
||||
- Conversion rate (free → pro)
|
||||
- Refund rate
|
||||
|
||||
### Product Metrics
|
||||
- Daily/weekly active users
|
||||
- Feature usage (which features are popular)
|
||||
- Error rates
|
||||
- Update adoption rate
|
||||
|
||||
### Marketing Metrics
|
||||
- Website visitors
|
||||
- Download counts
|
||||
- Trial starts
|
||||
- Email open/click rates
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Cloud Tier (v2)
|
||||
- User accounts required
|
||||
- Connection sync across devices
|
||||
- Saved queries library
|
||||
- Team sharing features
|
||||
- Subscription billing via DodoPayments
|
||||
|
||||
### Additional Payment Options
|
||||
- Regional pricing
|
||||
- Team/volume licenses
|
||||
- Educational discounts
|
||||
- Lifetime deals (AppSumo?)
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Exact DodoPayments webhook event names (check their docs)
|
||||
- [ ] License key format preference
|
||||
- [ ] Offline grace period duration (7 or 14 days?)
|
||||
- [ ] Domain name for marketing site
|
||||
- [ ] App signing certificates (macOS notarization, Windows signing)
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [DodoPayments Docs](https://docs.dodopayments.com)
|
||||
- [Next.js App Router](https://nextjs.org/docs/app)
|
||||
- [NextAuth.js](https://next-auth.js.org/)
|
||||
- [Resend](https://resend.com/docs)
|
||||
- [Supabase](https://supabase.com/docs)
|
||||
- [node-machine-id](https://www.npmjs.com/package/node-machine-id)
|
||||
|
||||
---
|
||||
|
||||
*Document created: November 2024*
|
||||
*Last updated: November 2024*
|
||||
279
docs/features.md
279
docs/features.md
|
|
@ -1,279 +0,0 @@
|
|||
# data-peek Feature Overview
|
||||
|
||||
> A minimal, fast, beautiful PostgreSQL client for developers who want to quickly peek at their data.
|
||||
|
||||
---
|
||||
|
||||
## Product Summary
|
||||
|
||||
**data-peek** is a lightweight desktop database client designed for developers who need quick, frictionless access to their PostgreSQL databases. Unlike bloated alternatives like pgAdmin or DBeaver, data-peek focuses on speed, simplicity, and a keyboard-first experience.
|
||||
|
||||
**Target Audience:** Developers, data engineers, backend engineers, and anyone who needs to quickly query and explore PostgreSQL databases without the overhead of enterprise tools.
|
||||
|
||||
**Platforms:** macOS (Apple Silicon + Intel), Windows, Linux
|
||||
|
||||
---
|
||||
|
||||
## Key Value Propositions
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Lightning Fast** | Opens in under 2 seconds. No splash screens, no waiting. |
|
||||
| **Zero Configuration** | Connect and query immediately. No complex setup required. |
|
||||
| **Keyboard-First** | Power users can do everything without touching the mouse. |
|
||||
| **Beautiful & Modern** | Dark and light themes with a clean, distraction-free UI. |
|
||||
| **Privacy-First** | No telemetry, no tracking. Your data stays on your machine. |
|
||||
| **Secure** | Connection credentials are encrypted locally. |
|
||||
| **Pay Once, Own Forever** | No subscriptions. One-time purchase with 1 year of updates. |
|
||||
|
||||
---
|
||||
|
||||
## Pricing
|
||||
|
||||
### Free Tier
|
||||
Get started at no cost:
|
||||
- 2 database connections
|
||||
- 50 query history items
|
||||
- 3 editor tabs
|
||||
- 1 schema for ER diagrams
|
||||
- CSV/JSON export
|
||||
|
||||
### Pro License — ~~$99~~ $29 (Early Bird)
|
||||
Unlock everything:
|
||||
- **Unlimited** connections
|
||||
- **Unlimited** query history
|
||||
- **Unlimited** tabs
|
||||
- **Unlimited** ER diagrams
|
||||
- Inline data editing (INSERT/UPDATE/DELETE)
|
||||
- Query execution plans (EXPLAIN/ANALYZE)
|
||||
- 3 device activations
|
||||
- 1 year of updates
|
||||
- **Pay once, use forever**
|
||||
|
||||
### Cloud (Coming Soon)
|
||||
For power users and teams:
|
||||
- Everything in Pro
|
||||
- Sync connections across devices
|
||||
- Cloud-saved queries
|
||||
- Team sharing
|
||||
- ~$5-8/month
|
||||
|
||||
---
|
||||
|
||||
## Feature List
|
||||
|
||||
### Connection Management
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Quick Connection Setup** | Add connections with host, port, database, user, and password — or paste a connection string |
|
||||
| **Connection String Parsing** | Paste any PostgreSQL connection URL and auto-fill all fields |
|
||||
| **Test Before Save** | Verify connections work before adding them |
|
||||
| **Encrypted Storage** | Credentials stored securely with encryption |
|
||||
| **SSL Support** | Connect to SSL-enabled databases |
|
||||
| **Connection Switcher** | Quickly switch between multiple database connections |
|
||||
| **Edit & Delete** | Manage saved connections with ease |
|
||||
|
||||
### Query Editor
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Monaco Editor** | Same editor engine that powers VS Code |
|
||||
| **SQL Syntax Highlighting** | Full SQL syntax highlighting with PostgreSQL support |
|
||||
| **Smart Autocomplete** | Schema-aware suggestions for tables, columns, and SQL keywords |
|
||||
| **Multi-Tab Support** | Work on multiple queries simultaneously with independent tabs |
|
||||
| **Query Formatting** | Auto-format SQL with `Cmd/Ctrl + Shift + F` |
|
||||
| **Run Query** | Execute with `Cmd/Ctrl + Enter` |
|
||||
| **Collapsible Editor** | Minimize the editor to focus on results |
|
||||
|
||||
### Results Viewer
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Data Table View** | View results in a clean, sortable table |
|
||||
| **Data Type Indicators** | Color-coded badges showing column types |
|
||||
| **Query Metrics** | See row count and query execution time |
|
||||
| **Pagination** | Navigate large result sets with customizable page sizes |
|
||||
| **Copy Cell** | Click any cell to copy its value |
|
||||
| **Copy Row as JSON** | Export individual rows as JSON objects |
|
||||
| **Export to CSV** | Download results as CSV files |
|
||||
| **Export to JSON** | Download results as JSON files |
|
||||
| **NULL Styling** | Clear visual distinction for NULL values |
|
||||
| **Foreign Key Navigation** | Click FK cells to view related records |
|
||||
| **JSON/JSONB Viewer** | Expand and inspect JSON columns inline |
|
||||
|
||||
### Schema Explorer
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Tree View Navigation** | Browse schemas, tables, and views hierarchically |
|
||||
| **Column Details** | See column names, data types, and constraints |
|
||||
| **Primary Key Indicators** | Visual markers for primary key columns |
|
||||
| **Nullable Indicators** | See which columns allow NULL values |
|
||||
| **Foreign Key Display** | View foreign key relationships |
|
||||
| **Table Search** | Filter tables by name with instant search |
|
||||
| **Click to Query** | Click any table to generate a SELECT query |
|
||||
| **Schema Refresh** | Reload schema after database changes |
|
||||
|
||||
### Query History
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Auto-Save** | Every executed query is automatically saved |
|
||||
| **Persistent Storage** | History survives app restarts |
|
||||
| **Query Metadata** | See execution time, row count, and status for each query |
|
||||
| **Quick Load** | Click any history item to load it into the editor |
|
||||
| **Copy to Clipboard** | Copy previous queries without loading |
|
||||
| **Clear History** | Remove all or individual history items |
|
||||
| **Query Type Badges** | Visual indicators for SELECT, INSERT, UPDATE, DELETE |
|
||||
| **Relative Timestamps** | "5 minutes ago", "yesterday", etc. |
|
||||
|
||||
### Inline Data Editing
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Edit Cells** | Double-click to modify cell values directly |
|
||||
| **Add Rows** | Insert new records with a visual form |
|
||||
| **Delete Rows** | Remove records with confirmation |
|
||||
| **SQL Preview** | Review generated SQL before executing changes |
|
||||
| **Batch Operations** | Queue multiple changes before committing |
|
||||
| **Discard Changes** | Undo pending edits before saving |
|
||||
| **Type-Safe Editing** | Input validation based on column data types |
|
||||
|
||||
### ER Diagram Visualization
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Visual Schema Map** | See your database structure as an interactive diagram |
|
||||
| **Table Nodes** | Each table displays all columns with types |
|
||||
| **Relationship Lines** | Foreign key connections visualized as links |
|
||||
| **Primary Key Highlights** | Yellow indicators for PK columns |
|
||||
| **Foreign Key Highlights** | Blue indicators for FK columns |
|
||||
| **Pan & Zoom** | Navigate large schemas with ease |
|
||||
| **Mini Map** | Overview navigation for complex databases |
|
||||
|
||||
### Query Execution Plans
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **EXPLAIN Visualization** | See query execution plans in a visual tree |
|
||||
| **Node Type Coloring** | Color-coded operations (scans, joins, sorts) |
|
||||
| **Cost Analysis** | View estimated vs actual costs |
|
||||
| **Performance Metrics** | Execution time breakdown by operation |
|
||||
| **Buffer Statistics** | I/O and memory usage details |
|
||||
| **Expandable Nodes** | Drill into plan details |
|
||||
|
||||
### User Interface
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Dark Mode** | Easy on the eyes for long coding sessions |
|
||||
| **Light Mode** | Clean, bright interface when you prefer it |
|
||||
| **System Preference** | Automatically match your OS theme |
|
||||
| **Resizable Panels** | Drag to resize sidebar and editor |
|
||||
| **Collapsible Sidebar** | Maximize workspace when needed |
|
||||
| **Loading States** | Clear feedback during operations |
|
||||
| **Error Handling** | Helpful error messages with details |
|
||||
| **Empty States** | Guided prompts when there's no data |
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Cmd/Ctrl + Enter` | Execute query |
|
||||
| `Cmd/Ctrl + Shift + F` | Format SQL |
|
||||
| `Cmd/Ctrl + P` | Open connection picker |
|
||||
| `Cmd/Ctrl + S` | Save query to file |
|
||||
| `Cmd/Ctrl + O` | Open query from file |
|
||||
| `Cmd/Ctrl + Shift + 1-9` | Switch between connections |
|
||||
|
||||
---
|
||||
|
||||
## Technical Highlights
|
||||
|
||||
| Aspect | Details |
|
||||
|--------|---------|
|
||||
| **Framework** | Electron with React 19 |
|
||||
| **Editor** | Monaco (VS Code engine) |
|
||||
| **Database Driver** | Native PostgreSQL (pg) |
|
||||
| **Local Storage** | SQLite for history and settings |
|
||||
| **Security** | Encrypted credential storage |
|
||||
| **Build Targets** | macOS DMG, Windows exe/msi, Linux AppImage |
|
||||
|
||||
---
|
||||
|
||||
## What data-peek is NOT
|
||||
|
||||
To set clear expectations:
|
||||
|
||||
- **Not a database admin tool** — Focus is on querying and exploring, not server management
|
||||
- **Not a data migration tool** — No import/export of entire databases
|
||||
- **Not multi-database** — PostgreSQL only (MySQL/SQLite coming in future versions)
|
||||
- **Not enterprise software** — Built for individual developers (team features coming with Cloud tier)
|
||||
|
||||
---
|
||||
|
||||
## Comparison with Alternatives
|
||||
|
||||
| Feature | data-peek | pgAdmin | DBeaver | TablePlus |
|
||||
|---------|-----------|---------|---------|-----------|
|
||||
| Startup Time | < 2s | 5-10s | 10-15s | 2-3s |
|
||||
| Memory Usage | Low | High | Very High | Low |
|
||||
| Learning Curve | Minimal | Steep | Steep | Minimal |
|
||||
| Price | Free + $29 Pro | Free | Free/Paid | $69 |
|
||||
| PostgreSQL Focus | Yes | Yes | No | No |
|
||||
| ER Diagrams | Yes | Yes | Yes | Yes |
|
||||
| Inline Editing | Yes | Yes | Yes | Yes |
|
||||
| Query Plans | Yes | Yes | Yes | Limited |
|
||||
| Modern UI | Yes | No | No | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Coming Soon
|
||||
|
||||
Features planned for future releases:
|
||||
|
||||
- MySQL and SQLite support
|
||||
- SSH tunnel connections
|
||||
- Saved queries / snippets library
|
||||
- Query cancellation
|
||||
- CSV data import
|
||||
- Connection groups/folders
|
||||
- **Cloud Sync** — Sync connections and saved queries across devices
|
||||
- **Team Features** — Share queries and connections with your team
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
*[Add screenshots here]*
|
||||
|
||||
- Connection dialog
|
||||
- Query editor with results
|
||||
- Schema explorer tree
|
||||
- ER diagram view
|
||||
- Query execution plan
|
||||
- Inline data editing
|
||||
- Dark/Light theme comparison
|
||||
|
||||
---
|
||||
|
||||
## One-Liner Descriptions
|
||||
|
||||
For various marketing contexts:
|
||||
|
||||
**Tagline:**
|
||||
> Peek at your data. Fast.
|
||||
|
||||
**Short (10 words):**
|
||||
> A fast, beautiful PostgreSQL client for developers who value simplicity.
|
||||
|
||||
**Medium (25 words):**
|
||||
> data-peek is a lightweight PostgreSQL desktop client with a modern UI, keyboard shortcuts, and features like ER diagrams and query plans — without the bloat.
|
||||
|
||||
**Long (50 words):**
|
||||
> data-peek is the PostgreSQL client developers actually want to use. Lightning-fast startup, Monaco-powered SQL editor, visual ER diagrams, query execution plans, inline data editing, and a beautiful dark/light UI. No telemetry, no subscriptions, no bloat. Pay once, own forever. Available for macOS, Windows, and Linux.
|
||||
|
||||
---
|
||||
|
||||
*Document generated: November 2025*
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# Future Plan
|
||||
|
||||
## Quick Wins
|
||||
- [x] Query history - browse and re-run previous queries
|
||||
- [x] Export results to CSV/JSON
|
||||
- [x] Table search - quickly find tables in large schemas
|
||||
- [x] Dark/light theme toggle (already in settings)
|
||||
- [x] Connection picker (Cmd+P) and quick switching (Cmd+Shift+1-9)
|
||||
|
||||
## Medium Effort
|
||||
- [ ] Saved queries/snippets - bookmark frequently used queries
|
||||
- [ ] Column statistics - show min/max/null counts/distinct values
|
||||
- [x] Connection folders/groups - organize many connections
|
||||
- [ ] Multi-statement query execution (run multiple queries separated by `;`)
|
||||
|
||||
## Bigger Features
|
||||
- [x] ERD visualization - see table relationships visually
|
||||
- [x] Query execution plan viewer - analyze query performance (EXPLAIN ANALYZE)
|
||||
- [ ] Data visualization - charts from query results
|
||||
- [x] SQL autocomplete improvements (table/column suggestions) - already implemented
|
||||
|
|
@ -1,329 +0,0 @@
|
|||
# data-peek Release Guide
|
||||
|
||||
This document covers everything needed to ship data-peek for macOS, Windows, and Linux.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR — Can I Ship for Free?
|
||||
|
||||
**Yes.** Here's the reality:
|
||||
|
||||
| Platform | Free Option | Paid Option | User Experience Difference |
|
||||
|----------|-------------|-------------|---------------------------|
|
||||
| **Linux** | ✅ No signing needed | N/A | No difference |
|
||||
| **macOS** | ✅ Ad-hoc signing | $99/year Apple Developer | Warning dialog vs no warning |
|
||||
| **Windows** | ✅ Unsigned | ~$200-400/year certificate | SmartScreen warning vs trusted |
|
||||
|
||||
Most open-source Electron apps ship unsigned. Users can bypass warnings.
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Details
|
||||
|
||||
### Linux — No Signing Required ✅
|
||||
|
||||
Linux doesn't require code signing. Your current setup is ready:
|
||||
- AppImage: Works out of the box
|
||||
- Snap: Works out of the box
|
||||
- Deb: Works out of the box
|
||||
|
||||
**Action needed:** None. Just build and distribute.
|
||||
|
||||
---
|
||||
|
||||
### macOS — Options
|
||||
|
||||
#### Option 1: Unsigned/Ad-hoc (Free) ✅
|
||||
|
||||
Users will see: *"data-peek can't be opened because Apple cannot check it for malicious software"*
|
||||
|
||||
**Workaround for users:**
|
||||
1. Right-click the app → Open (first time only)
|
||||
2. Or: `xattrs -cr /Applications/data-peek.app`
|
||||
|
||||
**Pros:** Free, works fine for developer tools
|
||||
**Cons:** Scary warning, extra step for users
|
||||
|
||||
**Current config already supports this** — just build and distribute.
|
||||
|
||||
#### Option 2: Apple Developer Program ($99/year)
|
||||
|
||||
Users will see: No warning, app opens normally.
|
||||
|
||||
**What you get:**
|
||||
- Developer ID certificate for signing
|
||||
- Notarization (Apple scans your app)
|
||||
- No Gatekeeper warnings
|
||||
|
||||
**When to consider:** If you want a polished user experience or plan to distribute via Mac App Store.
|
||||
|
||||
---
|
||||
|
||||
### Windows — Options
|
||||
|
||||
#### Option 1: Unsigned (Free) ✅
|
||||
|
||||
Users will see: *"Windows protected your PC — Microsoft Defender SmartScreen prevented an unrecognized app from starting"*
|
||||
|
||||
**Workaround for users:**
|
||||
1. Click "More info"
|
||||
2. Click "Run anyway"
|
||||
|
||||
**Pros:** Free
|
||||
**Cons:** SmartScreen warning scares users, some corporate machines block unsigned apps
|
||||
|
||||
**Note:** SmartScreen builds reputation over time. After enough users download and run your app without issues, warnings may reduce.
|
||||
|
||||
#### Option 2: Code Signing Certificate ($200-600/year)
|
||||
|
||||
Traditional EV (Extended Validation) certificates from:
|
||||
- DigiCert (~$400-600/year)
|
||||
- Sectigo (~$200-400/year)
|
||||
- SSL.com (~$200/year)
|
||||
|
||||
**Pros:** Immediate SmartScreen trust (EV certs), professional appearance
|
||||
**Cons:** Expensive, annual renewal
|
||||
|
||||
#### Option 3: Azure Trusted Signing (~$10/month) 💡
|
||||
|
||||
Microsoft's newer, cheaper alternative:
|
||||
- Pay-as-you-go pricing
|
||||
- Works with electron-builder
|
||||
- Builds SmartScreen reputation faster than unsigned
|
||||
|
||||
**Pros:** Cheap, modern, Microsoft-backed
|
||||
**Cons:** Requires Azure account, slightly more setup
|
||||
|
||||
#### Option 4: SignPath (Free for Open Source) 💡
|
||||
|
||||
[SignPath.io](https://signpath.io) offers free code signing for open-source projects.
|
||||
|
||||
**Requirements:**
|
||||
- Public GitHub repository
|
||||
- OSS license (MIT qualifies)
|
||||
- Apply and get approved
|
||||
|
||||
**Pros:** Completely free
|
||||
**Cons:** Approval process, builds must go through their CI
|
||||
|
||||
---
|
||||
|
||||
## Recommended Approach
|
||||
|
||||
### For Initial Release (v1.0)
|
||||
|
||||
Ship unsigned on all platforms:
|
||||
|
||||
1. **Linux** — No changes needed
|
||||
2. **macOS** — Users right-click → Open (one time)
|
||||
3. **Windows** — Users click "More info" → "Run anyway"
|
||||
|
||||
Include clear installation instructions in README.
|
||||
|
||||
### For Future Releases
|
||||
|
||||
Consider signing when:
|
||||
- You have paying users (Pro tier)
|
||||
- Download volume justifies the cost
|
||||
- Corporate users need signed apps
|
||||
|
||||
---
|
||||
|
||||
## Release Checklist
|
||||
|
||||
### Pre-Release
|
||||
|
||||
- [ ] Update `electron-builder.yml`:
|
||||
- [ ] Change `appId` to `com.datapeek.app`
|
||||
- [ ] Remove `electronDownload.mirror` line
|
||||
- [ ] Update `publish` config (see below)
|
||||
- [ ] Update `package.json`:
|
||||
- [ ] Set proper `author`
|
||||
- [ ] Set `homepage` to GitHub repo
|
||||
- [ ] Add LICENSE file (MIT)
|
||||
- [ ] Add README with screenshots and install instructions
|
||||
- [ ] Test builds on each platform
|
||||
|
||||
### electron-builder.yml Updates
|
||||
|
||||
```yaml
|
||||
appId: com.datapeek.app
|
||||
productName: data-peek
|
||||
|
||||
# ... keep existing config ...
|
||||
|
||||
# For GitHub Releases (recommended)
|
||||
publish:
|
||||
provider: github
|
||||
owner: Rohithgilla12
|
||||
repo: data-peek
|
||||
|
||||
# Remove this line:
|
||||
# electronDownload:
|
||||
# mirror: https://npmmirror.com/mirrors/electron/
|
||||
```
|
||||
|
||||
### Building Releases
|
||||
|
||||
```bash
|
||||
# From apps/desktop directory
|
||||
|
||||
# macOS (builds for current architecture)
|
||||
pnpm build:mac
|
||||
|
||||
# macOS universal (Intel + Apple Silicon)
|
||||
# Add to electron-builder.yml under mac:
|
||||
# target:
|
||||
# - target: dmg
|
||||
# arch: [x64, arm64]
|
||||
|
||||
# Windows
|
||||
pnpm build:win
|
||||
|
||||
# Linux
|
||||
pnpm build:linux
|
||||
```
|
||||
|
||||
### Creating a GitHub Release
|
||||
|
||||
1. Tag your release:
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
2. Go to GitHub → Releases → Draft new release
|
||||
|
||||
3. Upload build artifacts:
|
||||
- `data-peek-desktop-1.0.0.dmg` (macOS)
|
||||
- `data-peek-desktop-1.0.0-setup.exe` (Windows)
|
||||
- `data-peek-desktop-1.0.0.AppImage` (Linux)
|
||||
- `data-peek-desktop-1.0.0.snap` (Linux)
|
||||
- `data-peek-desktop-1.0.0.deb` (Linux)
|
||||
|
||||
4. Write release notes and publish
|
||||
|
||||
---
|
||||
|
||||
## CI/CD (Optional but Recommended)
|
||||
|
||||
Automate builds with GitHub Actions. Create `.github/workflows/release.yml`:
|
||||
|
||||
```yaml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
|
||||
- run: pnpm install
|
||||
|
||||
- name: Build
|
||||
run: pnpm --filter @data-peek/desktop build
|
||||
|
||||
- name: Build Electron app
|
||||
run: |
|
||||
cd apps/desktop
|
||||
pnpm exec electron-builder --publish never
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ matrix.os }}
|
||||
path: apps/desktop/dist/*.{dmg,exe,AppImage,snap,deb}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation Instructions for Users
|
||||
|
||||
Include this in your README:
|
||||
|
||||
### macOS
|
||||
|
||||
1. Download `data-peek-desktop-x.x.x.dmg`
|
||||
2. Open the DMG and drag to Applications
|
||||
3. **First launch:** Right-click the app → Click "Open" → Click "Open" again
|
||||
|
||||
(This is required because the app is not notarized)
|
||||
|
||||
### Windows
|
||||
|
||||
1. Download `data-peek-desktop-x.x.x-setup.exe`
|
||||
2. Run the installer
|
||||
3. **If you see SmartScreen warning:** Click "More info" → "Run anyway"
|
||||
|
||||
### Linux
|
||||
|
||||
**AppImage:**
|
||||
```bash
|
||||
chmod +x data-peek-desktop-x.x.x.AppImage
|
||||
./data-peek-desktop-x.x.x.AppImage
|
||||
```
|
||||
|
||||
**Snap:**
|
||||
```bash
|
||||
sudo snap install data-peek-desktop-x.x.x.snap --dangerous
|
||||
```
|
||||
|
||||
**Deb:**
|
||||
```bash
|
||||
sudo dpkg -i data-peek-desktop-x.x.x.deb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cost Summary
|
||||
|
||||
| Approach | Cost | Notes |
|
||||
|----------|------|-------|
|
||||
| Ship unsigned | $0 | Users see warnings |
|
||||
| macOS only signing | $99/year | Apple Developer Program |
|
||||
| Windows only signing | $10-400/year | Azure TS or traditional cert |
|
||||
| Both platforms signed | $109-500/year | Combined |
|
||||
| SignPath (Windows) | $0 | Free for OSS, requires approval |
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Auto-Updates
|
||||
|
||||
Current config uses `electron-updater`. For unsigned apps:
|
||||
- macOS: Auto-updates work but user must approve
|
||||
- Windows: Auto-updates work
|
||||
- Linux: AppImage supports auto-updates via `electron-updater`
|
||||
|
||||
### Mac App Store
|
||||
|
||||
Requires Apple Developer Program + additional review process. Consider only if:
|
||||
- You want discoverability
|
||||
- You're okay with Apple's 15-30% cut
|
||||
- Your app meets their guidelines
|
||||
|
||||
### Windows Store
|
||||
|
||||
Possible but requires Microsoft Partner account. Similar considerations to Mac App Store.
|
||||
333
docs/scope.md
333
docs/scope.md
|
|
@ -1,333 +0,0 @@
|
|||
# data-peek v1.0 Scope Document
|
||||
|
||||
> A minimal, fast, beautiful Postgres client for developers who want to quickly peek at their data.
|
||||
|
||||
## Target User
|
||||
|
||||
Developers who need a lightweight alternative to pgAdmin/DBeaver for day-to-day queries.
|
||||
|
||||
## Core Principles
|
||||
|
||||
- **Simple over feature-rich** — Do less, but do it well
|
||||
- **Fast to open, fast to query** — No bloat
|
||||
- **Keyboard-first** — Power users shouldn't need a mouse
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Choice | Why |
|
||||
|-------|--------|-----|
|
||||
| Desktop | Electron | Community, TS-native, contributor-friendly |
|
||||
| Frontend | React + TypeScript | Industry standard, type safety |
|
||||
| Bundler | electron-vite | Fast, modern, great DX |
|
||||
| UI | shadcn/ui + Tailwind | Beautiful, accessible, customizable |
|
||||
| State | Zustand | Simple, minimal boilerplate |
|
||||
| Query Editor | Monaco | VS Code engine, excellent SQL support |
|
||||
| Local Storage | SQLite (better-sqlite3) | Query history, cached schemas |
|
||||
| Config | electron-store | Encrypted connection credentials |
|
||||
| Database Client | pg | Native Postgres driver |
|
||||
|
||||
---
|
||||
|
||||
## In Scope (v1.0)
|
||||
|
||||
### Connection Management
|
||||
|
||||
- [x] Add new Postgres connection (host, port, database, user, password)
|
||||
- [x] Edit existing connection
|
||||
- [x] Delete connection
|
||||
- [x] Test connection before saving
|
||||
- [x] Encrypted credential storage (electron-store)
|
||||
- [x] Connection list in sidebar
|
||||
- [x] SSL connection support (basic)
|
||||
|
||||
### Query Editor
|
||||
|
||||
- [x] Monaco editor with SQL syntax highlighting
|
||||
- [x] Single query tab (multi-tab is v1.1) — *Actually implemented multi-tab!*
|
||||
- [x] Run query: `Cmd/Ctrl + Enter`
|
||||
- [ ] Clear editor: `Cmd/Ctrl + L`
|
||||
- [x] Basic error display with message
|
||||
|
||||
### Results Viewer
|
||||
|
||||
- [x] Table view with columns and rows
|
||||
- [x] Column headers with data type indicators
|
||||
- [x] Row count and query duration display
|
||||
- [x] Client-side pagination (100 rows per page)
|
||||
- [x] Copy cell value on click
|
||||
- [x] Copy row as JSON
|
||||
- [x] Export results to CSV
|
||||
- [x] Export results to JSON
|
||||
- [x] NULL value indicator styling
|
||||
|
||||
### Schema Explorer
|
||||
|
||||
- [x] Tree view: Connection → Schemas → Tables/Views
|
||||
- [x] Show columns under each table:
|
||||
- Column name
|
||||
- Data type
|
||||
- Nullable indicator
|
||||
- Primary key indicator
|
||||
- [x] Click table name to insert into editor
|
||||
- [x] Refresh schema button
|
||||
- [x] Collapse/expand nodes
|
||||
|
||||
### Query History
|
||||
|
||||
- [x] Auto-save last 100 executed queries (local SQLite)
|
||||
- [x] Store: query text, timestamp, duration, row count
|
||||
- [x] Display in sidebar panel
|
||||
- [x] Click to load query into editor
|
||||
- [x] Clear history option
|
||||
- [x] Persist across sessions
|
||||
|
||||
### UI/UX
|
||||
|
||||
- [x] Dark mode only (light mode is v1.1) — *Actually implemented light mode + system preference!*
|
||||
- [x] Resizable sidebar (drag handle)
|
||||
- [x] Loading states for queries
|
||||
- [x] Empty states with helpful messages
|
||||
- [x] Error states with clear messaging
|
||||
- [x] Keyboard shortcuts:
|
||||
- `Cmd/Ctrl + Enter` — Run query
|
||||
- `Cmd/Ctrl + S` — Save query to file
|
||||
- `Cmd/Ctrl + O` — Open query from file
|
||||
- `Cmd/Ctrl + ,` — Open settings
|
||||
|
||||
### Platform Support
|
||||
|
||||
- [x] macOS build (DMG, Apple Silicon + Intel)
|
||||
- [x] Linux build (AppImage)
|
||||
- [x] Windows build (exe/msi)
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (v1.0)
|
||||
|
||||
These features are explicitly deferred to future versions. When tempted to add them, resist.
|
||||
|
||||
| Feature | Target Version | Status |
|
||||
|---------|----------------|--------|
|
||||
| Multiple query tabs | v1.1 | ✅ Done |
|
||||
| MySQL adapter | v1.1 | |
|
||||
| SQLite adapter | v1.1 | |
|
||||
| Light theme | v1.1 | ✅ Done |
|
||||
| Connection groups/folders | v1.1 | |
|
||||
| Autocomplete (tables/columns) | v1.2 | ✅ Done (schema-aware) |
|
||||
| Query formatting/beautify | v1.2 | ✅ Done |
|
||||
| Saved queries / snippets library | v1.2 | |
|
||||
| SSH tunnel connections | v1.2 | |
|
||||
| Query cancellation | v1.2 | |
|
||||
| Table data editing (inline UPDATE/INSERT) | v2.0 | ✅ Done |
|
||||
| ER diagram visualization | v2.0 | ✅ Done |
|
||||
| Query EXPLAIN/ANALYZE visualizer | v2.0 | ✅ Done |
|
||||
| Import data from CSV | v2.0 | |
|
||||
| Database diff tool | v2.0 | |
|
||||
| Cloud sync (connections, history) | Pro | |
|
||||
| Team workspaces | Pro | |
|
||||
| Shared query library | Pro | |
|
||||
| SSO / SAML | Pro | |
|
||||
| Audit logs | Pro | |
|
||||
|
||||
---
|
||||
|
||||
## Technical Boundaries
|
||||
|
||||
- **No ORM** — Raw `pg` client only, keep it simple
|
||||
- **No server component** — Pure desktop app, no backend
|
||||
- **No auth in v1** — Local app, no user accounts
|
||||
- **No auto-updates in v1** — Manual download for updates
|
||||
- **No telemetry** — Privacy first, no tracking
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Renderer Process (React) │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐│
|
||||
│ │ Sidebar │ │ Query Editor│ │ Results Viewer ││
|
||||
│ │ - Conns │ │ (Monaco) │ │ - Table/JSON/Export ││
|
||||
│ │ - Schema │ │ │ │ ││
|
||||
│ │ - History │ │ │ │ ││
|
||||
│ └─────────────┘ └─────────────┘ └─────────────────────────┘│
|
||||
│ │ │
|
||||
│ ┌───────▼───────┐ │
|
||||
│ │ IPC Bridge │ │
|
||||
│ └───────┬───────┘ │
|
||||
└────────────────────────────┼────────────────────────────────┘
|
||||
│
|
||||
┌────────────────────────────┼────────────────────────────────┐
|
||||
│ Main Process (Node.js) │
|
||||
│ ┌───────▼───────┐ │
|
||||
│ │ Service Layer │ │
|
||||
│ └───────┬───────┘ │
|
||||
│ ┌───────────────────┼───────────────────┐ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │Connection│ │ Query │ │ Schema │ │
|
||||
│ │ Manager │ │ Engine │ │ Explorer │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
|
||||
│ └──────────────────┼───────────────────┘ │
|
||||
│ ┌─────▼─────┐ │
|
||||
│ │ Postgres │ │
|
||||
│ │ Adapter │ │
|
||||
│ └───────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Layout
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ data-peek [Connection: prod-db ▼] [⚙️] │
|
||||
├────────────────┬─────────────────────────────────────────────────┤
|
||||
│ │ │
|
||||
│ CONNECTIONS │ SELECT * FROM users │
|
||||
│ ├─ prod-db ● │ WHERE created_at > '2024-01-01' │
|
||||
│ ├─ staging │ LIMIT 100; │
|
||||
│ └─ local │ │
|
||||
│ │ [▶ Run] │
|
||||
│ SCHEMA ├─────────────────────────────────────────────────┤
|
||||
│ ▼ public │ │
|
||||
│ ├─ users │ id │ name │ email │ created_at │
|
||||
│ ├─ orders │ ───┼─────────┼─────────────────┼────────────── │
|
||||
│ └─ products │ 1 │ Alice │ alice@test.com │ 2024-01-15 │
|
||||
│ │ 2 │ Bob │ bob@test.com │ 2024-02-20 │
|
||||
│ HISTORY │ 3 │ Carol │ carol@test.com │ 2024-03-10 │
|
||||
│ ├─ SELECT ... │ ... │
|
||||
│ ├─ UPDATE ... ├─────────────────────────────────────────────────┤
|
||||
│ └─ ... │ ✓ 100 rows │ 24ms │ Page 1/10 │ [CSV] [JSON] │
|
||||
└────────────────┴─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Milestones
|
||||
|
||||
### M1: Foundation
|
||||
- [x] Electron app scaffolded with electron-vite
|
||||
- [x] Monorepo structure with pnpm workspaces
|
||||
- [x] Can connect to a Postgres database
|
||||
- [x] Can run a hardcoded query and log results
|
||||
|
||||
### M2: Connection Management
|
||||
- [x] Connection form UI (add/edit)
|
||||
- [x] Connection list in sidebar
|
||||
- [x] Test connection functionality
|
||||
- [x] Encrypted storage with electron-store
|
||||
- [x] Delete connection
|
||||
|
||||
### M3: Schema Explorer
|
||||
- [x] Fetch and display schemas
|
||||
- [x] Fetch and display tables per schema
|
||||
- [x] Fetch and display columns per table
|
||||
- [x] Tree view component with expand/collapse
|
||||
- [x] Click to insert table name
|
||||
|
||||
### M4: Query Editor
|
||||
- [x] Monaco editor integration
|
||||
- [x] SQL syntax highlighting
|
||||
- [x] Run query button
|
||||
- [x] Keyboard shortcut (Cmd+Enter)
|
||||
- [x] Error display
|
||||
|
||||
### M5: Results Viewer
|
||||
- [x] Table component for results
|
||||
- [x] Column headers
|
||||
- [x] Pagination
|
||||
- [x] Copy cell/row
|
||||
- [x] Export to CSV
|
||||
- [x] Export to JSON
|
||||
|
||||
### M6: Query History
|
||||
- [x] SQLite setup for local storage
|
||||
- [x] Auto-save executed queries
|
||||
- [x] History list in sidebar
|
||||
- [x] Click to load into editor
|
||||
- [x] Clear history
|
||||
|
||||
### M7: Polish & Release
|
||||
- [x] Keyboard shortcuts complete
|
||||
- [x] Loading states
|
||||
- [x] Error handling
|
||||
- [x] Empty states
|
||||
- [x] Build for macOS
|
||||
- [x] Build for Linux
|
||||
- [x] Build for Windows
|
||||
- [ ] README with screenshots
|
||||
- [ ] v1.0 release
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
v1.0 is complete when:
|
||||
|
||||
1. ✅ Can connect to a Postgres database
|
||||
2. ✅ Can browse schema (schemas, tables, columns)
|
||||
3. ✅ Can write and run SQL queries
|
||||
4. ✅ Can view results in a table
|
||||
5. ✅ Can export results to CSV/JSON
|
||||
6. ✅ Query history persists across sessions
|
||||
7. ✅ App opens in under 2 seconds
|
||||
8. ✅ Feels snappy — no UI lag
|
||||
9. ✅ Builds available for macOS, Linux, Windows
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals
|
||||
|
||||
To keep scope tight, these are explicitly NOT goals for v1:
|
||||
|
||||
- Being a full database administration tool
|
||||
- Competing with DataGrip/TablePlus on features
|
||||
- Supporting every Postgres feature
|
||||
- Having a plugin system
|
||||
- Mobile support
|
||||
|
||||
---
|
||||
|
||||
## Open Source Strategy
|
||||
|
||||
### License
|
||||
MIT — Maximum adoption, contributor-friendly.
|
||||
|
||||
### Contribution Guidelines
|
||||
- PRs welcome for bug fixes and v1 scope items
|
||||
- Features outside v1 scope will be reviewed for v1.1+
|
||||
- All PRs require tests for new functionality
|
||||
|
||||
### Future Monetization (Post v1)
|
||||
Open core model:
|
||||
- Free: Everything in v1 scope, forever
|
||||
- Pro: Cloud sync, team features, priority support
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- [Electron Documentation](https://www.electronjs.org/docs)
|
||||
- [electron-vite](https://electron-vite.org/)
|
||||
- [Monaco Editor React](https://github.com/suren-atoyan/monaco-react)
|
||||
- [shadcn/ui](https://ui.shadcn.com/)
|
||||
- [Zustand](https://github.com/pmndrs/zustand)
|
||||
- [node-postgres (pg)](https://node-postgres.com/)
|
||||
|
||||
---
|
||||
|
||||
## Changelog
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2024-XX-XX | Initial scope document created |
|
||||
| 2025-11-28 | Updated implementation status - v1.0 scope complete + bonus v2.0 features |
|
||||
|
||||
---
|
||||
|
||||
*Remember: When in doubt, leave it out. Ship v1, then iterate.*
|
||||
|
|
@ -1,831 +0,0 @@
|
|||
# data-peek Web Integration Guide
|
||||
|
||||
> Complete walkthrough for testing, purchasing, and integrating the license system with the desktop app.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Feature Checklist](#feature-checklist)
|
||||
2. [Environment Setup](#environment-setup)
|
||||
3. [Purchase Flow](#purchase-flow)
|
||||
4. [Desktop App Integration](#desktop-app-integration)
|
||||
5. [API Reference](#api-reference)
|
||||
6. [Testing Scenarios](#testing-scenarios)
|
||||
|
||||
---
|
||||
|
||||
## Feature Checklist
|
||||
|
||||
### Marketing Site
|
||||
|
||||
| Page | Feature | Status |
|
||||
|------|---------|--------|
|
||||
| **Landing** | Hero section with animations | ⬜ |
|
||||
| **Landing** | Features grid (12 cards) | ⬜ |
|
||||
| **Landing** | Pricing cards (Free vs Pro) | ⬜ |
|
||||
| **Landing** | Comparison table | ⬜ |
|
||||
| **Landing** | FAQ accordion | ⬜ |
|
||||
| **Landing** | CTA section | ⬜ |
|
||||
| **Landing** | Footer with links | ⬜ |
|
||||
| **Landing** | Mobile responsive | ⬜ |
|
||||
| **Download** | Platform cards (macOS, Windows, Linux) | ⬜ |
|
||||
| **Download** | Download links work | ⬜ |
|
||||
| **Download** | System requirements shown | ⬜ |
|
||||
|
||||
### Screenshots to Add
|
||||
|
||||
Replace these placeholder locations with actual screenshots:
|
||||
|
||||
| Location | File | Recommended Size |
|
||||
|----------|------|------------------|
|
||||
| Hero section | `public/screenshots/hero.png` | 1920×1080 |
|
||||
| Query Editor | `public/screenshots/editor.png` | 1200×750 |
|
||||
| ER Diagrams | `public/screenshots/erd.png` | 1200×750 |
|
||||
|
||||
### Backend APIs
|
||||
|
||||
| Endpoint | Test Command | Expected |
|
||||
|----------|--------------|----------|
|
||||
| License Validate | `curl -X POST /api/license/validate` | Returns validation status |
|
||||
| License Activate | `curl -X POST /api/license/activate` | Creates activation |
|
||||
| License Deactivate | `curl -X POST /api/license/deactivate` | Removes activation |
|
||||
| Update Check | `curl /api/updates/check?version=1.0.0` | Returns update info |
|
||||
| Dodo Webhook | POST with signature | Creates license |
|
||||
|
||||
---
|
||||
|
||||
## Environment Setup
|
||||
|
||||
### 1. Database (Supabase or Neon)
|
||||
|
||||
```bash
|
||||
# Create a PostgreSQL database, then:
|
||||
cd apps/web
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
Add your database URL:
|
||||
```env
|
||||
DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require"
|
||||
```
|
||||
|
||||
Run migrations:
|
||||
```bash
|
||||
pnpm db:push
|
||||
```
|
||||
|
||||
### 2. Clerk Authentication
|
||||
|
||||
1. Create account at [clerk.com](https://clerk.com)
|
||||
2. Create a new application
|
||||
3. Copy keys to `.env.local`:
|
||||
|
||||
```env
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
|
||||
CLERK_SECRET_KEY="sk_test_..."
|
||||
```
|
||||
|
||||
### 3. DodoPayments
|
||||
|
||||
1. Create account at [dodopayments.com](https://dodopayments.com)
|
||||
2. Create a product:
|
||||
- Name: `data-peek Pro License`
|
||||
- Type: One-time payment
|
||||
- Price: $29 (or $99 regular)
|
||||
3. Set up webhook:
|
||||
- URL: `https://your-domain.com/api/webhooks/dodo`
|
||||
- Events: `payment.completed`, `payment.refunded`
|
||||
4. Copy credentials:
|
||||
|
||||
```env
|
||||
DODO_API_KEY="..."
|
||||
DODO_WEBHOOK_SECRET="..."
|
||||
```
|
||||
|
||||
### 4. Resend (Email)
|
||||
|
||||
1. Create account at [resend.com](https://resend.com)
|
||||
2. Verify your domain
|
||||
3. Create API key:
|
||||
|
||||
```env
|
||||
RESEND_API_KEY="re_..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Purchase Flow
|
||||
|
||||
### How It Works
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PURCHASE FLOW │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. User clicks "Get Pro — $29" on website │
|
||||
│ ↓ │
|
||||
│ 2. Redirected to DodoPayments checkout │
|
||||
│ ↓ │
|
||||
│ 3. User completes payment │
|
||||
│ ↓ │
|
||||
│ 4. DodoPayments sends webhook to /api/webhooks/dodo │
|
||||
│ ↓ │
|
||||
│ 5. Backend creates customer + license in database │
|
||||
│ ↓ │
|
||||
│ 6. Welcome email sent with license key │
|
||||
│ ↓ │
|
||||
│ 7. User enters key in desktop app → activated! │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### License Key Format
|
||||
|
||||
```
|
||||
DPRO-XXXX-XXXX-XXXX-XXXX
|
||||
```
|
||||
|
||||
- Prefix: `DPRO` (Pro), `DTEAM` (Team), `DENT` (Enterprise)
|
||||
- 4 groups of 4 alphanumeric characters
|
||||
- No confusing characters (0, O, 1, I, l excluded)
|
||||
|
||||
### Setting Up the Buy Button
|
||||
|
||||
Update the pricing component to link to DodoPayments:
|
||||
|
||||
```tsx
|
||||
// src/components/marketing/pricing.tsx
|
||||
|
||||
// Replace href with your DodoPayments checkout link
|
||||
{
|
||||
cta: 'Get Pro License',
|
||||
href: 'https://checkout.dodopayments.com/buy/your-product-id',
|
||||
// Or use their SDK for embedded checkout
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Purchases Locally
|
||||
|
||||
1. Use DodoPayments test mode
|
||||
2. Use webhook testing tool (ngrok or similar):
|
||||
|
||||
```bash
|
||||
ngrok http 3000
|
||||
# Use the ngrok URL for webhook endpoint
|
||||
```
|
||||
|
||||
3. Make a test purchase
|
||||
4. Check database for new license:
|
||||
|
||||
```bash
|
||||
pnpm db:studio
|
||||
# Opens Drizzle Studio to inspect data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Desktop App Integration
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
cd apps/desktop
|
||||
pnpm add node-machine-id
|
||||
```
|
||||
|
||||
### 2. Add License Types
|
||||
|
||||
Create `src/shared/license.ts`:
|
||||
|
||||
```typescript
|
||||
export interface LicenseInfo {
|
||||
valid: boolean
|
||||
plan: 'free' | 'pro' | 'team' | 'enterprise'
|
||||
status: 'active' | 'revoked' | 'expired'
|
||||
updatesUntil: string
|
||||
activationsUsed: number
|
||||
activationsMax: number
|
||||
}
|
||||
|
||||
export interface ActivationInfo {
|
||||
id: string
|
||||
deviceId: string
|
||||
deviceName: string | null
|
||||
activatedAt: string
|
||||
}
|
||||
|
||||
export interface ActivateResponse {
|
||||
success: boolean
|
||||
activation?: ActivationInfo
|
||||
license?: LicenseInfo
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create License Service (Main Process)
|
||||
|
||||
Create `src/main/license.ts`:
|
||||
|
||||
```typescript
|
||||
import { machineIdSync } from 'node-machine-id'
|
||||
import { app } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import os from 'os'
|
||||
|
||||
const store = new Store()
|
||||
const API_BASE = 'https://datapeek.app/api' // or your domain
|
||||
|
||||
// Get unique device identifier
|
||||
export function getDeviceId(): string {
|
||||
return machineIdSync()
|
||||
}
|
||||
|
||||
export function getDeviceInfo() {
|
||||
return {
|
||||
deviceId: getDeviceId(),
|
||||
deviceName: os.hostname(),
|
||||
os: process.platform, // darwin, win32, linux
|
||||
appVersion: app.getVersion(),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate license with server
|
||||
export async function validateLicense(licenseKey: string): Promise<LicenseInfo> {
|
||||
const response = await fetch(`${API_BASE}/license/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
licenseKey,
|
||||
deviceId: getDeviceId(),
|
||||
}),
|
||||
})
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Activate license on this device
|
||||
export async function activateLicense(licenseKey: string): Promise<ActivateResponse> {
|
||||
const deviceInfo = getDeviceInfo()
|
||||
|
||||
const response = await fetch(`${API_BASE}/license/activate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
licenseKey,
|
||||
...deviceInfo,
|
||||
}),
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
// Store license locally
|
||||
store.set('license', {
|
||||
key: licenseKey,
|
||||
...result.license,
|
||||
lastValidated: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Deactivate this device
|
||||
export async function deactivateLicense(licenseKey: string): Promise<{ success: boolean }> {
|
||||
const response = await fetch(`${API_BASE}/license/deactivate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
licenseKey,
|
||||
deviceId: getDeviceId(),
|
||||
}),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
store.delete('license')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
}
|
||||
|
||||
// Get stored license
|
||||
export function getStoredLicense() {
|
||||
return store.get('license') as StoredLicense | undefined
|
||||
}
|
||||
|
||||
// Check license on app startup
|
||||
export async function checkLicenseOnStartup(): Promise<LicenseStatus> {
|
||||
const stored = getStoredLicense()
|
||||
|
||||
if (!stored) {
|
||||
return { status: 'free', features: FREE_FEATURES }
|
||||
}
|
||||
|
||||
try {
|
||||
// Try online validation
|
||||
const result = await validateLicense(stored.key)
|
||||
|
||||
if (result.valid) {
|
||||
// Update cache
|
||||
store.set('license', {
|
||||
...stored,
|
||||
...result,
|
||||
lastValidated: new Date().toISOString(),
|
||||
})
|
||||
return { status: 'pro', features: PRO_FEATURES, license: result }
|
||||
} else {
|
||||
// License revoked or invalid
|
||||
store.delete('license')
|
||||
return { status: 'free', features: FREE_FEATURES, error: 'License invalid' }
|
||||
}
|
||||
} catch (error) {
|
||||
// Offline - use cached validation with grace period
|
||||
const lastValidated = new Date(stored.lastValidated)
|
||||
const gracePeriod = 14 * 24 * 60 * 60 * 1000 // 14 days
|
||||
|
||||
if (Date.now() - lastValidated.getTime() < gracePeriod) {
|
||||
return { status: 'pro', features: PRO_FEATURES, offline: true }
|
||||
}
|
||||
|
||||
return { status: 'free', features: FREE_FEATURES, error: 'License validation failed' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add IPC Handlers
|
||||
|
||||
In `src/main/index.ts`:
|
||||
|
||||
```typescript
|
||||
import {
|
||||
checkLicenseOnStartup,
|
||||
activateLicense,
|
||||
deactivateLicense,
|
||||
getStoredLicense,
|
||||
} from './license'
|
||||
|
||||
// License IPC handlers
|
||||
ipcMain.handle('license:check', async () => {
|
||||
return checkLicenseOnStartup()
|
||||
})
|
||||
|
||||
ipcMain.handle('license:activate', async (_, licenseKey: string) => {
|
||||
return activateLicense(licenseKey)
|
||||
})
|
||||
|
||||
ipcMain.handle('license:deactivate', async (_, licenseKey: string) => {
|
||||
return deactivateLicense(licenseKey)
|
||||
})
|
||||
|
||||
ipcMain.handle('license:get', async () => {
|
||||
return getStoredLicense()
|
||||
})
|
||||
```
|
||||
|
||||
### 5. Update Preload Script
|
||||
|
||||
In `src/preload/index.ts`:
|
||||
|
||||
```typescript
|
||||
// Add to the API object
|
||||
license: {
|
||||
check: () => ipcRenderer.invoke('license:check'),
|
||||
activate: (key: string) => ipcRenderer.invoke('license:activate', key),
|
||||
deactivate: (key: string) => ipcRenderer.invoke('license:deactivate', key),
|
||||
get: () => ipcRenderer.invoke('license:get'),
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Create License Store (Renderer)
|
||||
|
||||
Create `src/renderer/src/stores/license-store.ts`:
|
||||
|
||||
```typescript
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface LicenseState {
|
||||
status: 'loading' | 'free' | 'pro' | 'team' | 'enterprise'
|
||||
license: LicenseInfo | null
|
||||
isOffline: boolean
|
||||
error: string | null
|
||||
|
||||
// Actions
|
||||
checkLicense: () => Promise<void>
|
||||
activateLicense: (key: string) => Promise<{ success: boolean; error?: string }>
|
||||
deactivateLicense: () => Promise<void>
|
||||
|
||||
// Feature checks
|
||||
isPro: () => boolean
|
||||
canUseFeature: (feature: string) => boolean
|
||||
}
|
||||
|
||||
// Feature limits for free tier
|
||||
const FREE_LIMITS = {
|
||||
connections: 2,
|
||||
queryHistory: 50,
|
||||
tabs: 3,
|
||||
erDiagrams: 1,
|
||||
}
|
||||
|
||||
export const useLicenseStore = create<LicenseState>((set, get) => ({
|
||||
status: 'loading',
|
||||
license: null,
|
||||
isOffline: false,
|
||||
error: null,
|
||||
|
||||
checkLicense: async () => {
|
||||
try {
|
||||
const result = await window.api.license.check()
|
||||
set({
|
||||
status: result.status,
|
||||
license: result.license ?? null,
|
||||
isOffline: result.offline ?? false,
|
||||
error: result.error ?? null,
|
||||
})
|
||||
} catch (error) {
|
||||
set({ status: 'free', error: 'Failed to check license' })
|
||||
}
|
||||
},
|
||||
|
||||
activateLicense: async (key: string) => {
|
||||
try {
|
||||
const result = await window.api.license.activate(key)
|
||||
if (result.success) {
|
||||
set({
|
||||
status: result.license?.plan ?? 'pro',
|
||||
license: result.license ?? null,
|
||||
error: null,
|
||||
})
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false, error: result.error }
|
||||
} catch (error) {
|
||||
return { success: false, error: 'Activation failed' }
|
||||
}
|
||||
},
|
||||
|
||||
deactivateLicense: async () => {
|
||||
const license = get().license
|
||||
if (license) {
|
||||
await window.api.license.deactivate(license.key)
|
||||
}
|
||||
set({ status: 'free', license: null })
|
||||
},
|
||||
|
||||
isPro: () => {
|
||||
const status = get().status
|
||||
return status === 'pro' || status === 'team' || status === 'enterprise'
|
||||
},
|
||||
|
||||
canUseFeature: (feature: string) => {
|
||||
const isPro = get().isPro()
|
||||
if (isPro) return true
|
||||
|
||||
// Check free tier limits
|
||||
switch (feature) {
|
||||
case 'unlimited-connections':
|
||||
case 'unlimited-history':
|
||||
case 'unlimited-tabs':
|
||||
case 'unlimited-erd':
|
||||
case 'inline-editing':
|
||||
case 'query-plans':
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
},
|
||||
}))
|
||||
```
|
||||
|
||||
### 7. Create License Dialog Component
|
||||
|
||||
Create `src/renderer/src/components/license-dialog.tsx`:
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react'
|
||||
import { useLicenseStore } from '@/stores/license-store'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Key, Check, AlertCircle, ExternalLink } from 'lucide-react'
|
||||
|
||||
interface LicenseDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function LicenseDialog({ open, onOpenChange }: LicenseDialogProps) {
|
||||
const [licenseKey, setLicenseKey] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const { status, license, activateLicense, deactivateLicense } = useLicenseStore()
|
||||
const isPro = status !== 'free' && status !== 'loading'
|
||||
|
||||
const handleActivate = async () => {
|
||||
if (!licenseKey.trim()) return
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = await activateLicense(licenseKey.trim())
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
if (result.success) {
|
||||
setLicenseKey('')
|
||||
onOpenChange(false)
|
||||
} else {
|
||||
setError(result.error ?? 'Activation failed')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeactivate = async () => {
|
||||
setIsLoading(true)
|
||||
await deactivateLicense()
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Key className="w-5 h-5" />
|
||||
License
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{isPro ? (
|
||||
// Pro license view
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<div className="flex items-center gap-2 text-green-500 mb-2">
|
||||
<Check className="w-4 h-4" />
|
||||
<span className="font-medium">Pro License Active</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Updates until: {new Date(license?.updatesUntil ?? '').toLocaleDateString()}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Activations: {license?.activationsUsed} / {license?.activationsMax}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={handleDeactivate}
|
||||
disabled={isLoading}
|
||||
>
|
||||
Deactivate This Device
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
// Free tier view
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">License Key</label>
|
||||
<Input
|
||||
placeholder="DPRO-XXXX-XXXX-XXXX-XXXX"
|
||||
value={licenseKey}
|
||||
onChange={(e) => setLicenseKey(e.target.value.toUpperCase())}
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 text-red-500 text-sm">
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleActivate}
|
||||
disabled={isLoading || !licenseKey.trim()}
|
||||
>
|
||||
{isLoading ? 'Activating...' : 'Activate License'}
|
||||
</Button>
|
||||
|
||||
<div className="text-center">
|
||||
<a
|
||||
href="https://datapeek.app/#pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
Get a Pro license
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Feature Gating Example
|
||||
|
||||
```tsx
|
||||
// Example: Gating the "Add Connection" button
|
||||
|
||||
import { useLicenseStore } from '@/stores/license-store'
|
||||
|
||||
function ConnectionList() {
|
||||
const { isPro, canUseFeature } = useLicenseStore()
|
||||
const connections = useConnectionStore((s) => s.connections)
|
||||
|
||||
const canAddConnection = isPro() || connections.length < 2
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* ... connection list ... */}
|
||||
|
||||
<Button
|
||||
onClick={handleAddConnection}
|
||||
disabled={!canAddConnection}
|
||||
>
|
||||
Add Connection
|
||||
{!canAddConnection && (
|
||||
<Badge variant="secondary" className="ml-2">Pro</Badge>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{!canAddConnection && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Free tier limited to 2 connections.
|
||||
<a href="#" onClick={openLicenseDialog}>Upgrade to Pro</a>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
### POST /api/license/validate
|
||||
|
||||
Validate a license key and check if device is activated.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"licenseKey": "DPRO-XXXX-XXXX-XXXX-XXXX",
|
||||
"deviceId": "unique-machine-id"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"plan": "pro",
|
||||
"status": "active",
|
||||
"updatesUntil": "2025-11-28T00:00:00.000Z",
|
||||
"activationsUsed": 1,
|
||||
"activationsMax": 3
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/license/activate
|
||||
|
||||
Activate a license on a new device.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"licenseKey": "DPRO-XXXX-XXXX-XXXX-XXXX",
|
||||
"deviceId": "unique-machine-id",
|
||||
"deviceName": "MacBook Pro",
|
||||
"os": "darwin",
|
||||
"appVersion": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"activation": {
|
||||
"id": "uuid",
|
||||
"deviceId": "unique-machine-id",
|
||||
"deviceName": "MacBook Pro",
|
||||
"activatedAt": "2024-11-28T00:00:00.000Z"
|
||||
},
|
||||
"license": {
|
||||
"plan": "pro",
|
||||
"updatesUntil": "2025-11-28T00:00:00.000Z",
|
||||
"activationsUsed": 1,
|
||||
"activationsMax": 3
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/license/deactivate
|
||||
|
||||
Deactivate a device.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"licenseKey": "DPRO-XXXX-XXXX-XXXX-XXXX",
|
||||
"deviceId": "unique-machine-id"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"activationsRemaining": 2
|
||||
}
|
||||
```
|
||||
|
||||
### GET /api/updates/check
|
||||
|
||||
Check for app updates.
|
||||
|
||||
**Request:**
|
||||
```
|
||||
GET /api/updates/check?version=1.0.0&platform=macos-arm
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"hasUpdate": true,
|
||||
"latestVersion": "1.1.0",
|
||||
"currentVersion": "1.0.0",
|
||||
"downloadUrl": "https://...",
|
||||
"releaseNotes": "Bug fixes and improvements",
|
||||
"forceUpdate": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Manual Test Checklist
|
||||
|
||||
| Scenario | Steps | Expected Result |
|
||||
|----------|-------|-----------------|
|
||||
| **Fresh install (free)** | Open app with no license | Free tier limits apply |
|
||||
| **Valid activation** | Enter valid license key | Unlocks Pro features |
|
||||
| **Invalid key** | Enter random key | Shows error message |
|
||||
| **Max activations** | Activate on 4th device | Shows "max reached" error |
|
||||
| **Deactivate** | Deactivate from settings | Reverts to free tier |
|
||||
| **Offline mode** | Disconnect internet, open app | Uses cached license (14 day grace) |
|
||||
| **Revoked license** | Revoke via webhook | Shows "revoked" error on next validation |
|
||||
| **Update check** | Use older version | Shows update available |
|
||||
|
||||
### Test License Keys
|
||||
|
||||
For development, you can manually insert test licenses:
|
||||
|
||||
```sql
|
||||
-- Insert test customer
|
||||
INSERT INTO customers (email, name)
|
||||
VALUES ('test@example.com', 'Test User');
|
||||
|
||||
-- Insert test license (get customer ID from above)
|
||||
INSERT INTO licenses (customer_id, license_key, plan, status, max_activations, updates_until)
|
||||
VALUES (
|
||||
'customer-uuid-here',
|
||||
'DPRO-TEST-TEST-TEST-TEST',
|
||||
'pro',
|
||||
'active',
|
||||
3,
|
||||
NOW() + INTERVAL '1 year'
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
- [ ] Database migrations run on production
|
||||
- [ ] Environment variables set on Vercel/hosting
|
||||
- [ ] DodoPayments webhook URL updated to production
|
||||
- [ ] Clerk production keys configured
|
||||
- [ ] Resend domain verified
|
||||
- [ ] Download links point to actual releases
|
||||
- [ ] Screenshots added to marketing site
|
||||
|
||||
---
|
||||
|
||||
*Document created: November 2024*
|
||||
Loading…
Reference in a new issue