project-brainstorm
Load when a user has a vague idea, problem, or inspiration but isn't sure what to build. Use before /project-specify when the project concept needs crystallizing from loose thoughts into something concrete.
revenuecat-integration
Load when integrating RevenueCat for in-app purchases and subscriptions. Applies when implementing iOS/Android subscriptions, entitlement checking, or purchase webhooks in React Native/Flutter.
Full skill instructions
Apply when implementing subscriptions or in-app purchases in React Native or Flutter apps.
npm install react-native-purchases
Android: Set launch mode to prevent purchase interruption:
<!-- AndroidManifest.xml -->
<activity android:name=".MainActivity" android:launchMode="standard" />
iOS: Enable In-App Purchase capability in Xcode.
import Purchases from 'react-native-purchases';
import { Platform } from 'react-native';
useEffect(() => {
Purchases.setLogLevel(Purchases.LOG_LEVEL.DEBUG);
if (Platform.OS === 'ios') {
Purchases.configure({ apiKey: 'ios_api_key' });
} else {
Purchases.configure({ apiKey: 'android_api_key' });
}
}, []);
import 'package:purchases_flutter/purchases_flutter.dart';
await Purchases.setLogLevel(LogLevel.debug);
if (defaultTargetPlatform == TargetPlatform.iOS) {
await Purchases.configure(PurchasesConfiguration('ios_key'));
} else {
await Purchases.configure(PurchasesConfiguration('android_key'));
}
export function useSubscription() {
const [isSubscribed, setIsSubscribed] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkSubscription();
}, []);
const checkSubscription = async () => {
try {
setLoading(true);
const info = await Purchases.getCustomerInfo();
// Check for active entitlement
setIsSubscribed(!!info.entitlements.active['premium']);
} catch (error) {
console.error('Error:', error);
} finally {
setLoading(false);
}
};
return { isSubscribed, loading, refresh: checkSubscription };
}
Future<bool> isPremium() async {
final info = await Purchases.getCustomerInfo();
return info.entitlements.active.containsKey('premium');
}
// Always verify on backend for sensitive operations
const response = await fetch(
`https://api.revenuecat.com/v1/subscribers/${userId}`,
{ headers: { Authorization: `Bearer ${REVENUECAT_API_KEY}` } }
);
const data = await response.json();
const isActive = Object.keys(data.subscriber.entitlements.active).length > 0;
export function Paywall() {
const [offerings, setOfferings] = useState(null);
const [purchasing, setPurchasing] = useState(false);
useEffect(() => {
loadOfferings();
}, []);
const loadOfferings = async () => {
const { current } = await Purchases.getOfferings();
setOfferings(current);
};
const purchase = async (pkg) => {
try {
setPurchasing(true);
const { customerInfo } = await Purchases.purchasePackage(pkg);
if (customerInfo.entitlements.active['premium']) {
Alert.alert('Success', 'Subscription activated!');
}
} catch (error) {
if (!error.userCancelled) {
Alert.alert('Error', error.message);
}
} finally {
setPurchasing(false);
}
};
if (!offerings) return <ActivityIndicator />;
return (
<View>
<Button
title={`Monthly - ${offerings.monthly?.localizedPriceString}`}
onPress={() => purchase(offerings.monthly)}
disabled={purchasing}
/>
<Button
title={`Annual - ${offerings.annual?.localizedPriceString}`}
onPress={() => purchase(offerings.annual)}
disabled={purchasing}
/>
</View>
);
}
app.post('/webhooks/revenuecat', async (req, res) => {
// Validate auth header
if (req.headers.authorization !== process.env.REVENUECAT_WEBHOOK_SECRET) {
return res.status(401).send('Unauthorized');
}
const { type, app_user_id } = req.body;
// Query RevenueCat for authoritative state
const subscriber = await fetchSubscriber(app_user_id);
const isActive = Object.keys(subscriber.entitlements.active).length > 0;
// Update your database
await db.users.update({
where: { id: app_user_id },
data: { isPremium: isActive },
});
res.status(200).send('OK');
});
| Event | When | Action |
|---|---|---|
INITIAL_PURCHASE | First subscription | Provision access |
RENEWAL | Auto-renewed | Log retention |
BILLING_ISSUE_DETECTED | Payment failed | Prompt to update card |
SUBSCRIPTION_EXPIRED | Cancelled + period ended | Revoke access |
CANCELLATION | User cancelled (still active) | Show retention offer |
Webhooks may be delivered multiple times. Track processed events:
const processed = await db.webhookEvents.findOne({ id: event.id });
if (processed) return res.status(200).send('Already processed');
await handleWebhook(event);
await db.webhookEvents.create({ id: event.id });
| Approach | Use Case | Limitations |
|---|---|---|
| RevenueCat Test Store | Early dev, no Apple/Google setup | No platform-specific features |
| Platform Sandboxes | Pre-launch, full integration | Metadata may not match production |
In RevenueCat Dashboard → Project Settings:
Anybody: All sandbox purchases grant entitlementsAllowed App User IDs only: Whitelist specific test usersNobody: Track purchases without granting accessWhen users are redirected to banking apps for payment verification, wrong launch mode cancels purchases:
<!-- MUST be standard or singleTop -->
<activity android:launchMode="standard" />
Detaching an entitlement from a product removes access for ALL existing customers instantly. Be careful when modifying entitlement-product mappings.
Use platform suffixes to avoid confusion:
monthly_ios, yearly_ios
monthly_android, yearly_android
Users keep access for ~16 days after failed payment. Check grace_period_expires_date in API response.
Events may arrive out of order. Always query RevenueCat API for current state rather than building state from events.
| Task | Pattern |
|---|---|
| Check subscription | customerInfo.entitlements.active['premium'] |
| Make purchase | Purchases.purchasePackage(pkg) |
| Load offerings | Purchases.getOfferings() |
| Backend verification | GET /v1/subscribers/{id} |
| Webhook auth | Check Authorization header |
| Test Store → Sandbox | Change API key before launch |
Load when a user has a vague idea, problem, or inspiration but isn't sure what to build. Use before /project-specify when the project concept needs crystallizing from loose thoughts into something concrete.
Load when using Docker for containerizing applications. Applies when creating Dockerfiles, docker-compose setups, multi-stage builds, or optimizing container images.
Enforce visual design quality in every UI file. Fires for .tsx, .jsx, .vue, .svelte, and similar component files. Ensures implementations express the project's design personality rather than producing generic/boilerplate UI.
Load after project.md exists to fill gaps through targeted Q&A. Use when project goals, target users, or scope feel underspecified. Run before project-context or project-architecture to prevent rework downstream.
DEPRECATED in Speck v7. Use /speck-skeptical-review + /speck-decision-log instead — they replace outline-style research mapping with structured alternative enumeration and decision locking. /story-plan now performs just-in-time research inline. This skill remains for v6 compatibility but redirect...
Load when building offline-first applications that work without network connectivity. Applies when implementing local storage, sync queues, conflict resolution, or background sync.
Load (Platform play level only) to define technical principles and architectural guardrails for the project. Produces constitution.md — optional but valuable when a team needs explicit coding standards, governance rules, or non-negotiable constraints. FIRST ACTION after loading: read template at ...
Load when integrating PostHog for product analytics and feature flags. Applies when implementing event tracking, A/B testing, session replay, or funnel analysis.
Load when all epics are validated and /audit has run for the project. Verifies the project meets its original vision and product-contract.md paid promise. Runs end-to-end JTBD smoke test via /larp across all personas. Declares a project-level readiness state. Produces project-validation-report.md...
Load for brownfield stories where existing code must be understood before planning. Use when a story extends, modifies, or integrates with significant existing functionality. Produces codebase-scan.md — skip for fully greenfield stories. FIRST ACTION after loading: read template at .speck/templat...
Load when building serverless applications with AWS Lambda, Vercel Functions, or Cloudflare Workers. Applies when implementing FaaS patterns, managing cold starts, or optimizing serverless costs.
Load after epic-tech-spec.md and epic-breakdown.md exist — quality check for consistency and completeness before any story work begins. Catches spec drift and missing coverage between the tech spec and story breakdown. FIRST ACTION after loading: read template at .speck/templates/epic/epic-analys...
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience