A modern, production-ready SaaS template built with SvelteKit 2, Svelte 5, Supabase, and Skeleton UI. Get your SaaS project up and running in hours, not months.
- π Sveltey - a SaaS Template built with SvelteKit
- Supabase Auth Integration: Email/password sign-up (with email confirmation) and OAuth providers via PKCE
- Protected Routes: Server-side route protection and session management in
hooks.server.ts - User Profiles: Profile page with display-name editing
- Password Reset: Request-link flow with secure password update page
- Skeleton UI: Beautiful, accessible components out of the box
- Dark/Light Mode: Built-in mode switching without flash-of-wrong-theme
- Comprehensive Themes: 22 beautiful handmade themes
- Responsive Design: Mobile-first, responsive layouts
- Loading States: Elegant loading indicators and transitions
- Toast Notifications: User-friendly feedback system
- Blog System: Built-in blog with markdown support
- SEO Optimized: Meta tags, Open Graph, and structured data
- Contact Form: Resend-powered contact form with validation, honeypot, and rate limiting
- Native Stripe Integration: Stripe Checkout subscriptions β no billing wrapper
- Subscription Plans: Configurable pricing tiers in
src/lib/config/plans.ts - Billing Management: Stripe Customer Portal for invoices, payment methods, and cancellation
- Webhook Handling: Signature-verified webhook mirroring subscription state into Postgres (RLS-safe)
- Admin Dashboard: Clean, intuitive admin interface
- User Analytics: Basic user metrics and insights
- Real-time Updates: Live data updates using Supabase real-time
- Data Visualization: Charts and graphs for key metrics
- Privacy-First Analytics: Built-in Plausible Analytics integration for GDPR-compliant tracking
# Clone the repository
git clone https://github.com/yourusername/sveltey.git
cd sveltey
# Install dependencies
npm install
# Edit .env with your Supabase, Resend and Stripe keys
# Start the development server
npm run devVisit http://localhost:5173 and start building your SaaS!
- Framework: SvelteKit 2 with Svelte 5
- Database: Supabase (PostgreSQL)
- Authentication: Supabase Auth
- UI Components: Skeleton UI
- Styling: Tailwind CSS
- Analytics: Plausible Analytics
- Email: Resend
- Deployment: Vercel/Netlify ready
- Payments: Stripe (native Checkout + Customer Portal + webhooks)
src/
βββ lib/
β βββ auth/ # Auth helpers (error mapping, redirect guards)
β βββ components/ # Reusable UI components (incl. auth/AuthForm)
β βββ config/ # plans.ts β subscription plan definitions
β βββ server/ # Server-only modules (stripe, supabase-admin)
β βββ posts/ # Markdown blog posts
β βββ blog.ts # Blog loaders
β βββ supabase-client.ts # Browser Supabase client
βββ routes/
β βββ api/ # API endpoints (auth handled per-endpoint)
β | βββ stripe/ # checkout, portal, webhook
β βββ (app)/ # Authenticated shell
| | βββ app/ # Dashboard, profile, billing (guarded in hooks)
| | βββ auth/ # login, register, reset, update-password, callback, logout
β βββ (marketing)/ # Public marketing pages
| βββ blog # Blog listing + [slug]
| βββ contact # Contact form (Resend)
| βββ pricing # Plans with Stripe Checkout buttons
| βββ privacy # Privacy policy page
| βββ terms # Terms of service page
βββ app.html # App shell (theme bootstrap script)
βββ app.css # Global styles
βββ hooks.server.ts # Supabase session + route guards
supabase/
βββ migrations/ # SQL migrations (subscriptions table + RLS)
Create a .env file in the root directory:
# Supabase
PUBLIC_SUPABASE_URL=your_supabase_url
PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
# Supabase service role (server-only; required for Stripe webhooks)
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
# Resend (contact form)
RESEND_API_KEY=your_resend_api_key
CONTACT_FROM_EMAIL="Sveltey Contact <onboarding@resend.dev>"
CONTACT_TO_EMAIL=where@to.deliver
# Stripe (see docs/PAYMENTS_SETUP.md)
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=your_webhook_secret
# Optional Stripe Price IDs (inline pricing used when unset)
STRIPE_PRICE_PRO=
STRIPE_PRICE_ENTERPRISE=- Create a new Supabase project
- Copy the project URL and anon key into
.env - Run the included SQL migration:
supabase/migrations/0001_subscriptions.sql(SQL editor orsupabase db push) - Set up your authentication providers (see
docs/AUTHENTICATION_SETUP.md)
Sveltey uses Resend for reliable email delivery with excellent developer experience.
- Create a Resend account at resend.com
- Get your API key from the Resend dashboard
- Add to environment variables:
RESEND_API_KEY=re_your_api_key_here
- Verify your domain (optional but recommended for production):
- Add your domain in the Resend dashboard
- Configure DNS records as instructed
- Contact Form Delivery: The
/contactform sends through a server action with validation, a honeypot field, and per-IP rate limiting - Graceful Dev Mode: Without
RESEND_API_KEY, submissions are logged to the server console and the UI says so β never a fake success - Simple API: Resend is imported statically in
src/routes/(marketing)/contact/+page.server.ts
See src/routes/(marketing)/contact/+page.server.ts for the full implementation
(validation, honeypot, rate limit, HTML escaping). The send call itself:
import { Resend } from 'resend';
const resend = new Resend(env.RESEND_API_KEY);
await resend.emails.send({
from: env.CONTACT_FROM_EMAIL ?? 'Sveltey Contact <onboarding@resend.dev>',
to: env.CONTACT_TO_EMAIL,
replyTo: submissionEmail,
subject: `Contact form: ${subject}`,
text,
html
});Create reusable email templates in src/lib/emails/:
// src/lib/emails/welcome.ts
export const welcomeEmail = (userName: string) => `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Welcome to Sveltey, ${userName}!</h1>
<p>Thank you for joining our platform. We're excited to have you on board.</p>
<a href="https://yourdomain.com/dashboard"
style="background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px;">
Get Started
</a>
</div>
`;Full walkthrough in docs/PAYMENTS_SETUP.md:
- Create a Stripe account and set
STRIPE_SECRET_KEY(+SUPABASE_SERVICE_ROLE_KEYfor webhooks) - Apply
supabase/migrations/0001_subscriptions.sql - Optionally create Products/Prices and set
STRIPE_PRICE_PRO/STRIPE_PRICE_ENTERPRISE - Forward webhooks (
checkout.session.completed,customer.subscription.*) and setSTRIPE_WEBHOOK_SECRET - Activate the Customer Portal link in the Stripe dashboard
Sveltey comes pre-configured with Plausible Analytics - a lightweight, privacy-focused analytics solution that's GDPR compliant by default.
The analytics script is already integrated in src/app.html:
<script defer data-domain="sveltey.dev" src="https://events.plygrnd.org/js/script.js"></script>- Self-hosted Plausible: If using your own Plausible instance, update the script source
- Plausible Cloud: Change to the official Plausible script:
<script defer data-domain="yourdomain.com" src="https://plausible.io/js/script.js"></script>
- Update Domain: Replace
data-domain="sveltey.dev"with your actual domain
- Privacy-First: No cookies, no personal data collection
- GDPR Compliant: No consent banners required
- Lightweight: Less than 1KB script size
- Real-time: Live visitor tracking
- Goal Tracking: Custom event tracking available
Add custom event tracking for user actions:
// Track custom events
plausible('signup', { props: { plan: 'premium' } });
plausible('purchase', { props: { amount: 99 } });To use a different analytics provider:
- Remove the Plausible script from
src/app.html - Add your preferred analytics script (Google Analytics, Fathom, etc.)
- Update the privacy policy accordingly
- Update
app.htmlfor favicon and meta tags - Modify
src/lib/config.tsfor app configuration - Customize colors in with a custom Skeleton UI theme
- Replace logo
- All components are in
src/lib/components/ - Skeleton UI provides the base component library
- Easy to theme and customize with CSS variables
Sveltey comes with a comprehensive SEO system built on top of svelte-meta-tags that provides automatic meta tag management, OpenGraph support, and Twitter Card integration.
The meta tag system in Sveltey uses a two-level approach:
- Base Meta Tags (
src/routes/+layout.ts) - Global defaults for your entire site - Page Meta Tags (
src/routes/*/+page.ts) - Page-specific overrides and additions
The base meta tags are defined in src/routes/+layout.ts and include:
const baseMetaTags = Object.freeze({
title: 'Sveltey - SvelteKit SaaS Template',
titleTemplate: '%s | Sveltey',
description: 'Your default site description...',
canonical: new URL(url.pathname, url.origin).href,
robots: 'index,follow',
keywords: ['SvelteKit', 'SaaS', 'template'],
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
title: 'Sveltey - SvelteKit SaaS Template',
description: 'Your OpenGraph description...',
siteName: 'Sveltey',
locale: 'en_US',
images: [
{
url: `${url.origin}/og-image.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey - SvelteKit SaaS Template',
type: 'image/jpeg'
}
]
},
twitter: {
cardType: 'summary_large_image',
site: '@sveltey_dev',
creator: '@sveltey_dev',
title: 'Sveltey - SvelteKit SaaS Template',
description: 'Your Twitter description...',
image: `${url.origin}/og-image.jpg`,
imageAlt: 'Sveltey - SvelteKit SaaS Template'
}
}) satisfies MetaTagsProps;Each page can override and extend the base meta tags by exporting a load function in its +page.ts file:
// src/routes/your-page/+page.ts
import type { MetaTagsProps } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
title: 'Your Page Title',
description: 'Specific description for this page',
keywords: ['additional', 'keywords', 'for', 'this', 'page'],
openGraph: {
title: 'Your Page Title - Brand Name',
description: 'OpenGraph description for social sharing',
type: 'article', // or 'website', 'product', etc.
images: [
{
url: 'https://your-domain.com/specific-og-image.jpg',
width: 1200,
height: 630,
alt: 'Description of your image'
}
]
},
twitter: {
title: 'Twitter-specific title',
description: 'Twitter-specific description'
},
// Additional meta tags
additionalMetaTags: [
{
name: 'author',
content: 'Your Name'
},
{
property: 'article:published_time',
content: '2024-01-01T00:00:00Z'
}
]
}) satisfies MetaTagsProps;
return {
pageMetaTags
};
};For dynamic pages (like blog posts), you can generate meta tags based on content:
// src/routes/blog/[slug]/+page.server.ts
export const load = async ({ params, url }) => {
const post = await getPostBySlug(params.slug);
const pageMetaTags = Object.freeze({
title: post.title,
description: post.excerpt,
canonical: new URL(`/blog/${params.slug}`, url.origin).href,
openGraph: {
type: 'article',
title: post.title,
description: post.excerpt,
url: new URL(`/blog/${params.slug}`, url.origin).href,
images: post.featuredImage
? [
{
url: post.featuredImage,
width: 1200,
height: 630,
alt: post.title
}
]
: undefined,
article: {
publishedTime: post.publishedAt,
authors: [post.author],
section: 'Technology',
tags: post.tags
}
}
}) satisfies MetaTagsProps;
return { post, pageMetaTags };
};Control search engine indexing per page:
const pageMetaTags = {
robots: 'noindex,nofollow', // Don't index this page
// or
robots: 'index,follow', // Index this page (default)
// or
robots: 'index,nofollow' // Index but don't follow links
};OpenGraph images are crucial for social media sharing and SEO. Sveltey provides a flexible system for managing these images.
Place your default OpenGraph image in the static folder:
static/
βββ og-image.jpg # Default 1200x630 image
βββ og-image-square.jpg # Optional square variant
βββ favicon.png
The default image is automatically referenced in your base meta tags:
// src/routes/+layout.ts
openGraph: {
images: [
{
url: `${url.origin}/og-image.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey - SvelteKit SaaS Template',
type: 'image/jpeg'
}
];
}Override the default image for specific pages:
// src/routes/pricing/+page.ts
const pageMetaTags = {
openGraph: {
images: [
{
url: `${url.origin}/og-pricing.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey Pricing Plans',
type: 'image/jpeg'
}
]
}
};For blog posts or dynamic content, you can generate or specify images dynamically:
// src/routes/blog/[slug]/+page.server.ts
const pageMetaTags = {
openGraph: {
images: post.featuredImage
? [
{
url: post.featuredImage,
width: 1200,
height: 630,
alt: post.title,
type: 'image/jpeg'
}
]
: [
{
url: `${url.origin}/og-blog-default.jpg`,
width: 1200,
height: 630,
alt: 'Sveltey Blog',
type: 'image/jpeg'
}
]
}
};- Recommended Size: 1200x630 pixels (1.91:1 aspect ratio)
- Minimum Size: 600x315 pixels
- Maximum Size: 8MB
- Format: JPG or PNG (JPG preferred for smaller file size)
// Example with multiple image variants
openGraph: {
images: [
{
url: `${url.origin}/og-image-large.jpg`,
width: 1200,
height: 630,
alt: 'Large image for Facebook, LinkedIn',
type: 'image/jpeg'
},
{
url: `${url.origin}/og-image-square.jpg`,
width: 1080,
height: 1080,
alt: 'Square image for Instagram, Twitter',
type: 'image/jpeg'
}
];
}For advanced use cases, you can generate images dynamically:
// src/routes/api/og/[slug]/+server.ts
export async function GET({ params, url }) {
const post = await getPostBySlug(params.slug);
// Generate image using libraries like @vercel/og or canvas
const image = await generateOGImage({
title: post.title,
author: post.author,
template: 'blog-post'
});
return new Response(image, {
headers: {
'Content-Type': 'image/png',
'Cache-Control': 'public, max-age=31536000, immutable'
}
});
}Then reference it in your meta tags:
openGraph: {
images: [
{
url: `${url.origin}/api/og/${params.slug}`,
width: 1200,
height: 630,
alt: post.title,
type: 'image/png'
}
];
}Use these tools to test your OpenGraph implementation:
- Facebook Debugger: https://developers.facebook.com/tools/debug/
- Twitter Card Validator: https://cards-dev.twitter.com/validator
- LinkedIn Post Inspector: https://www.linkedin.com/post-inspector/
openGraph: {
type: 'website', // website, article, product, etc.
title: 'Page Title', // Specific title for social sharing
description: 'Description', // Social media description
url: 'https://example.com', // Canonical URL
siteName: 'Site Name', // Your site/brand name
locale: 'en_US', // Language and region
// For articles
article: {
publishedTime: '2024-01-01T00:00:00Z',
modifiedTime: '2024-01-02T00:00:00Z',
authors: ['Author Name'],
section: 'Technology',
tags: ['svelte', 'sveltekit']
},
// For products
product: {
price: {
amount: '29.99',
currency: 'USD'
}
}
}Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
- Issues: Report bugs or request features via GitHub Issues
- SvelteKit - The amazing web framework
- Supabase - Backend as a Service
- Skeleton UI - UI component library
- Tailwind CSS - Utility-first CSS framework
- Stripe - Payment processing
Ready to launch your SaaS? Get started with Sveltey today!
