### Install @devvit/start Source: https://github.com/reddit/devvit/blob/main/packages/start/README.md Install the @devvit/start package as a development dependency. ```sh npm install -D @devvit/start ``` -------------------------------- ### Start Development Server Source: https://github.com/reddit/devvit/blob/main/packages/start/README.md Run this command to start the development server. The plugin automatically builds client and server in watch mode, rebuilding on changes. ```sh npm run dev ``` -------------------------------- ### Install Devvit CLI Locally Source: https://github.com/reddit/devvit/blob/main/packages/cli/README.md Run these commands in the repository root to prepare your local environment for the Devvit CLI. This includes installing dependencies, building the project, and installing development-specific packages. ```bash yarn && yarn build cd packages/cli yarn install:dev source ~/.zshrc ``` -------------------------------- ### context.redis Source: https://context7.com/reddit/devvit/llms.txt Provides a Redis-compatible client scoped to the installation. Supports strings, sorted sets, hashes, transactions, bitfields, pub/sub primitives, and key expiration. Also exposes `redis.global` for cross-installation keys. ```APIDOC ## context.redis ### Description Provides a Redis-compatible client scoped to the installation. Supports strings, sorted sets, hashes, transactions (MULTI/EXEC via `watch`), bitfields, pub/sub primitives, and key expiration. Also exposes `redis.global` for cross-installation keys. ### String Operations - **set(key: string, value: string, options?: SetOptions)**: Sets a string value. - **get(key: string)**: Gets a string value. - **incrBy(key: string, increment: number)**: Increments a string value by a given amount. ### Hash Operations - **hSet(key: string, field: string, value: string)**: Sets a field in a hash. - **hSet(key: string, fields: Record)**: Sets multiple fields in a hash. - **hGetAll(key: string)**: Gets all fields and values in a hash. ### Sorted Set Operations - **zAdd(key: string, members: Record)**: Adds members to a sorted set. - **zRange(key: string, start: number, stop: number, options?: ZRangeOptions)**: Gets a range of members from a sorted set. ### Key Expiration - **expire(key: string, seconds: number)**: Sets an expiration time for a key. ### Transactions - **watch(key: string)**: Watches a key for changes before starting a transaction. - **multi()**: Starts a transaction. - **exec()**: Executes a transaction. ``` -------------------------------- ### Store, Retrieve, List, and Delete JSON with kvStore Source: https://context7.com/reddit/devvit/llms.txt Use `kvStore` for simple per-installation JSON storage. Prefer `context.redis` for new apps. This example demonstrates storing, retrieving, listing, and deleting JSON values. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ kvStore: true }); Devvit.addTrigger({ event: 'PostSubmit', async onEvent(event, { kvStore }) { // Store JSON values await kvStore.put(`post:${event.post!.id}`, { title: event.post!.title, createdAt: new Date().toISOString(), }); // Retrieve const data = await kvStore.get<{ title: string; createdAt: string }>(`post:${event.post!.id}`); console.log(data?.title); // List all keys const keys = await kvStore.list(); console.log('Stored keys:', keys); // Delete await kvStore.delete(`post:${event.post!.id}`); }, }); export default Devvit; ``` -------------------------------- ### Devvit CLI Commands Source: https://context7.com/reddit/devvit/llms.txt The `devvit` CLI is used for authenticating, deploying, and managing Devvit apps. This includes installation, login, app creation, uploading, publishing, setting secrets, and viewing logs. ```bash # Install the CLI globally npm install -g devvit # Authenticate with your Reddit account devvit login # Create a new app from a template npx devvit new my-app # Upload (publish) your app for testing on a subreddit devvit upload # Deploy a new version to the Developer Portal devvit publish # Set an encrypted app-level secret (e.g. API key) devvit settings set openai-api-key # List configured settings for an installation devvit settings list --subreddit r/my-test-sub # View real-time logs from your deployed app devvit logs --subreddit r/my-test-sub # Run local playtest (requires a test subreddit) devvit playtest r/my-test-sub ``` -------------------------------- ### Declare App and Installation Settings with Devvit.addSettings() Source: https://context7.com/reddit/devvit/llms.txt Use Devvit.addSettings() to define configuration fields for your app. These settings can be app-level secrets or configurable per installation. Access values at runtime using context.settings.get() or context.settings.getAll(). ```typescript import { Devvit, SettingScope } from '@devvit/public-api'; Devvit.addSettings([ { type: 'string', name: 'openai-api-key', label: 'OpenAI API Key', scope: SettingScope.App, // Set once by developer via CLI: devvit settings set openai-api-key isSecret: true, }, { type: 'string', name: 'welcome-message', label: 'Welcome message shown to new members', scope: SettingScope.Installation, // Configurable per subreddit by the mod who installed the app defaultValue: 'Welcome to the community!', onValidate: ({ value }) => { if (value && value.length > 500) return 'Message must be 500 characters or less'; }, }, { type: 'number', name: 'post-limit', label: 'Max posts per user per day', scope: SettingScope.Installation, defaultValue: 5, }, { type: 'boolean', name: 'auto-flair', label: 'Automatically flair new posts', scope: SettingScope.Installation, defaultValue: false, }, ]); Devvit.addTrigger({ event: 'PostSubmit', async onEvent(event, context) { const { settings } = context; const apiKey = await settings.get('openai-api-key'); const welcome = await settings.get('welcome-message'); const autoFlair = await settings.get('auto-flair'); console.log({ apiKey, welcome, autoFlair }); }, }); export default Devvit; ``` -------------------------------- ### context.kvStore Source: https://context7.com/reddit/devvit/llms.txt Provides simple JSON key-value storage. Supports `get`, `put`, `delete`, and `list` operations. Prefer `context.redis` for new applications. ```APIDOC ## `context.kvStore` — Simple JSON key-value storage (legacy) Provides simple JSON key-value storage. Supports `get`, `put`, `delete`, and `list` operations. Prefer `context.redis` for new applications. ### Methods - `put(key: string, value: any): Promise`: Stores a JSON value associated with a key. - `get(key: string): Promise`: Retrieves a JSON value by its key. - `list(): Promise`: Returns a list of all stored keys. - `delete(key: string): Promise`: Removes a key-value pair. ### Example Usage ```typescript await kvStore.put(`post:${event.post!.id}`, { title: event.post!.title, createdAt: new Date().toISOString() }); const data = await kvStore.get<{ title: string; createdAt: string }>(`post:${event.post!.id}`); const keys = await kvStore.list(); await kvStore.delete(`post:${event.post!.id}`); ``` ``` -------------------------------- ### Upload Media Assets with media.upload() Source: https://context7.com/reddit/devvit/llms.txt Upload images and GIFs for use in posts or comments using `media.upload()`. Requires `media: true` in `Devvit.configure()`. This example shows uploading from a URL and using the media ID in a post. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ media: true, redditAPI: true }); Devvit.addMenuItem({ label: 'Upload Banner', location: 'subreddit', onPress: async (_, context) => { const { media, reddit, ui } = context; // Upload from URL const asset = await media.upload({ url: 'https://example.com/banner.png', type: 'image/png', }); // Use mediaId in a post const sub = await reddit.getCurrentSubreddit(); await reddit.submitPost({ subredditName: sub.name, title: 'New Banner!', text: `Uploaded media ID: ${asset.mediaId}\nURL: ${asset.mediaUrl}`, }); ui.showToast({ text: 'Banner uploaded and posted!', appearance: 'success' }); }, }); export default Devvit; ``` -------------------------------- ### Implement In-App Purchases with Payments API Source: https://context7.com/reddit/devvit/llms.txt Handle in-app purchases by registering a payment fulfillment handler with `addPaymentHandler`. This example shows fulfilling and refunding orders using Redis. ```typescript import { addPaymentHandler, usePayments, useProducts, OrderResultStatus } from '@devvit/payments'; import { Devvit, useState } from '@devvit/public-api'; // Must be at module level addPaymentHandler({ fulfillOrder: async (order, context) => { const { redis } = context; await redis.hSet(`user:${order.userId}:purchases`, { [order.sku]: '1' }); console.log(`Fulfilled order ${order.orderId} for SKU ${order.sku}`); }, refundOrder: async (order, context) => { const { redis } = context; await redis.hDel(`user:${order.userId}:purchases`, [order.sku]); console.log(`Refunded order ${order.orderId}`); }, }); Devvit.configure({ redditAPI: true }); Devvit.addCustomPostType({ name: 'Shop', render: (context) => { const { products } = useProducts(context); const payments = usePayments((result) => { if (result.status === OrderResultStatus.Success) { context.ui.showToast({ text: `Purchased ${result.sku}!`, appearance: 'success' }); } else { context.ui.showToast(`Purchase failed: ${result.errorMessage}`); } }); return ( Shop {(products ?? []).map((product) => ( {product.name} ))} ); }, }); export default Devvit; ``` -------------------------------- ### useInterval for Polling Timers in Custom Posts Source: https://context7.com/reddit/devvit/llms.txt Utilize `useInterval` to create recurring timers within custom posts, specified in milliseconds. The timer must be explicitly started using `.start()`. This hook is ideal for polling data or animating UI elements without relying on external push mechanisms. ```typescript import { Devvit, useState, useInterval } from '@devvit/public-api'; Devvit.configure({ redis: true }); Devvit.addCustomPostType({ name: 'Live Clock', render: (context) => { const [time, setTime] = useState(new Date().toISOString()); const ticker = useInterval(() => { setTime(new Date().toISOString()); }, 1000); // fires every 1 second ticker.start(); return ( {time} ); }, }); export default Devvit; ``` -------------------------------- ### Create Production Build Source: https://github.com/reddit/devvit/blob/main/packages/start/README.md Run this command to create optimized client and server bundles for production deployment. ```sh npm run build ``` -------------------------------- ### Configure @devvit/start Options Source: https://github.com/reddit/devvit/blob/main/packages/start/README.md Customize the @devvit/start plugin's behavior using optional configuration. Options include `logLevel`, and environment-specific Vite configurations for `client` and `server`. ```typescript devvit({ logLevel: 'warn', // Vite log level: 'info' | 'warn' | 'error' | 'silent' (default: 'warn') client: {}, // Vite EnvironmentOptions (merged into the client environment) server: {}, // Vite EnvironmentOptions (merged into the server environment) }); ``` -------------------------------- ### useWebView() Source: https://context7.com/reddit/devvit/llms.txt Mounts a full-screen HTML/JS web view from a bundled asset file. Use `postMessage` to communicate between the Devvit app and the web view. The web view sends messages back via `window.parent.postMessage`. ```APIDOC ## useWebView() ### Description Mounts a full-screen HTML/JS web view from a bundled asset file. Use `postMessage` to communicate between the Devvit app and the web view. The web view sends messages back via `window.parent.postMessage`. ### Usage ```typescript import { useWebView } from '@devvit/public-api'; const webView = useWebView({ url: 'index.html', onMessage: async (msg, hook) => { /* handle message */ }, onUnmount: async () => { /* handle unmount */ }, }); webView.mount(); webView.postMessage(message); ``` ### Parameters #### `useWebView` options - **url** (string) - Required - The path to the bundled HTML asset. - **onMessage** (async (msg: FromWebView, hook: WebViewHook) => void) - Optional - Callback function to handle messages from the web view. - **onUnmount** (async () => void) - Optional - Callback function when the web view is unmounted. ### Methods - **mount()**: Mounts the web view. - **postMessage(message: ToWebView)**: Sends a message to the web view. ``` -------------------------------- ### Devvit.configure() Source: https://context7.com/reddit/devvit/llms.txt Enables platform capabilities by accepting a configuration object that opts the app into specific Reddit platform plugins. ```APIDOC ## Devvit.configure() ### Description Must be called at the top level before using any capability. Accepts a configuration object that opts the app into specific Reddit platform plugins (Redis, Reddit API, HTTP, realtime, payments, media, user actions). ### Method `Devvit.configure(config: object)` ### Parameters #### Configuration Object - **redditAPI** (boolean) - Optional - Enables context.reddit client. - **redis** (boolean) - Optional - Enables context.redis client. - **realtime** (boolean) - Optional - Enables context.realtime and useChannel hook. - **media** (boolean) - Optional - Enables context.media.upload(). - **http** (object) - Optional - Allows configuration of allowed fetch domains. - **domains** (string[]) - Optional - Allowlisted fetch domains (max 10). - **userActions** (object) - Optional - Enables on-behalf-of-user actions. - **scopes** (string[]) - Optional - Specifies the scopes for user actions. ### Request Example ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redditAPI: true, redis: true, realtime: true, media: true, http: { domains: ['api.openai.com', 'api.weather.gov'], }, userActions: { scopes: ['submit_post', 'submit_comment'], }, }); ``` ``` -------------------------------- ### Full-Screen HTML Web View with `useWebView()` Source: https://context7.com/reddit/devvit/llms.txt Mount a full-screen HTML/JS web view from a bundled asset. Use `postMessage` for communication between the Devvit app and the web view. The web view sends messages back via `window.parent.postMessage`. Ensure `redis: true` is configured if using Redis. ```typescript import { Devvit, useState, useWebView } from '@devvit/public-api'; Devvit.configure({ redis: true }); type ToWebView = { type: 'INIT'; count: number }; type FromWebView = { type: 'INCREMENT' }; Devvit.addCustomPostType({ name: 'WebView Counter', render: (context) => { const [count, setCount] = useState(async () => { const v = await context.redis.get('wv_count'); return v ? parseInt(v) : 0; }); const webView = useWebView({ url: 'index.html', // bundled asset in webroot/ onMessage: async (msg, hook) => { if (msg.type === 'INCREMENT') { const next = count + 1; await context.redis.set('wv_count', String(next)); setCount(next); hook.postMessage({ type: 'INIT', count: next }); } }, onUnmount: async () => { context.ui.showToast('Web view closed'); }, }); return ( Count: {String(count)} ); }, }); export default Devvit; ``` -------------------------------- ### Configure Vite with @devvit/start Source: https://github.com/reddit/devvit/blob/main/packages/start/README.md Add the `devvit()` plugin to your `vite.config.ts`. It is recommended to place it last in the plugins array. ```typescript import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwind from '@tailwindcss/vite'; import { devvit } from '@devvit/start/vite'; export default defineConfig({ plugins: [react(), tailwind(), devvit()], }); ``` -------------------------------- ### Create Hard Link for Local Overrides Source: https://github.com/reddit/devvit/blob/main/packages/web-view-scripts/README.md Create a hard link to the bundled script for local overrides in Chrome. Symlinks may not work as expected. ```bash ln ../../../../work/reddit/src/devvit/packages/web-view-scripts/dist/scripts/devvit.v1.min.js devvit.v1.min.js\? ``` -------------------------------- ### Devvit.addSettings() Source: https://context7.com/reddit/devvit/llms.txt Declares configuration fields that appear in the Developer Portal for per-installation tuning or app-level secrets. Access values at runtime via context.settings.get() or context.settings.getAll(). ```APIDOC ## Devvit.addSettings() ### Description Declares configuration fields that appear in the Developer Portal for per-installation tuning (e.g., a custom city for a weather widget) or secure app-level secrets (e.g., an API key). Access values at runtime via `context.settings.get()` or `context.settings.getAll()`. ### Usage ```typescript import { Devvit, SettingScope } from '@devvit/public-api'; Devvit.addSettings([ { type: 'string', name: 'openai-api-key', label: 'OpenAI API Key', scope: SettingScope.App, isSecret: true, }, { type: 'string', name: 'welcome-message', label: 'Welcome message shown to new members', scope: SettingScope.Installation, defaultValue: 'Welcome to the community!', onValidate: ({ value }) => { if (value && value.length > 500) return 'Message must be 500 characters or less'; }, }, { type: 'number', name: 'post-limit', label: 'Max posts per user per day', scope: SettingScope.Installation, defaultValue: 5, }, { type: 'boolean', name: 'auto-flair', label: 'Automatically flair new posts', scope: SettingScope.Installation, defaultValue: false, }, ]); ``` ``` -------------------------------- ### context.media.upload() Source: https://context7.com/reddit/devvit/llms.txt Uploads media assets such as images and GIFs for use in posts or comments. Requires `media: true` in `Devvit.configure()`. ```APIDOC ## `context.media.upload()` — Upload media assets Uploads media assets like images and GIFs for use in posts or comments. Requires `media: true` in `Devvit.configure()`. ### Parameters - `options` (object) - Required - Options for uploading media. - `url` (string) - Required - The URL of the media asset. - `type` (string) - Required - The MIME type of the media (e.g., `image/png`, `image/gif`). ### Returns - `Promise`: A promise that resolves to an asset object containing `mediaId` and `mediaUrl`. - `mediaId` (string): The unique identifier for the uploaded media. - `mediaUrl` (string): The URL to access the uploaded media. ### Example Usage ```typescript const asset = await media.upload({ url: 'https://example.com/banner.png', type: 'image/png', }); // Use asset.mediaId and asset.mediaUrl ``` ``` -------------------------------- ### CLI Commands Source: https://context7.com/reddit/devvit/llms.txt Commands for the `devvit` CLI used to authenticate, deploy, and manage Devvit apps. ```APIDOC ## CLI Commands — `devvit` / `npx devvit` Commands for the `devvit` CLI used to authenticate, deploy, and manage Devvit apps against the production Reddit infrastructure. ### Authentication - `npm install -g devvit`: Installs the CLI globally. - `devvit login`: Authenticates with your Reddit account. ### App Management - `npx devvit new `: Creates a new app from a template. - `devvit upload`: Uploads (publishes) your app for testing on a subreddit. - `devvit publish`: Deploys a new version to the Developer Portal. ### Configuration & Settings - `devvit settings set `: Sets an encrypted app-level secret (e.g., API key). - `devvit settings list --subreddit `: Lists configured settings for an installation. ### Development & Debugging - `devvit logs --subreddit `: Views real-time logs from your deployed app. - `devvit playtest `: Runs a local playtest (requires a test subreddit). ``` -------------------------------- ### Run Devvit CLI with Production Backend Source: https://github.com/reddit/devvit/blob/main/packages/cli/README.md To use the local Devvit CLI with the production backend API, set the MY_PORTAL environment variable to 0 before executing the mydevvit command. ```bash MY_PORTAL=0 mydevvit [command] ``` -------------------------------- ### Devvit.createForm() / useForm() Source: https://context7.com/reddit/devvit/llms.txt Registers a form either at the module level (for menu items) or inline within a custom post render. Both return a FormKey that is passed to context.ui.showForm(). ```APIDOC ## Devvit.createForm() / useForm() ### Description `Devvit.createForm()` registers a form at the module level (usable from menu items). `useForm()` registers a form inline inside a custom post render. Both return a `FormKey` passed to `context.ui.showForm()`. ### Usage ```typescript import { Devvit, useForm } from '@devvit/public-api'; Devvit.configure({ redditAPI: true }); // Module-level form (for menu items) const reportForm = Devvit.createForm( { title: 'Report Issue', description: 'Tell us what went wrong', fields: [ { name: 'category', label: 'Category', type: 'select', options: [ { label: 'Bug', value: 'bug' }, { label: 'Spam', value: 'spam' }, ], required: true }, { name: 'description', label: 'Description', type: 'paragraph', required: true }, ], acceptLabel: 'Submit', cancelLabel: 'Cancel', }, async (values, context) => { const { reddit, ui } = context; await reddit.submitComment({ id: context.postId!, text: `[Report] ${values.category}: ${values.description}` }); ui.showToast({ text: 'Report submitted!', appearance: 'success' }); } ); // Inline form inside a custom post (useForm hook) Devvit.addCustomPostType({ name: 'Guest Book', render: (context) => { const signForm = useForm( { title: 'Sign the Guest Book', fields: [{ name: 'message', label: 'Your message', type: 'string', required: true }], }, async (values) => { await context.redis.rPush('guestbook', values.message as string); context.ui.showToast('Signed!'); } ); return ( ); }, }); ``` ``` -------------------------------- ### Demonstrate UI Effects with Devvit Source: https://context7.com/reddit/devvit/llms.txt Shows various UI interactions including toasts, navigation to URLs and Reddit objects. Ensure `redditAPI` is enabled for subreddit navigation. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redditAPI: true }); Devvit.addMenuItem({ label: 'Demo UI Effects', location: 'post', onPress: async (_, context) => { const { ui, reddit } = context; // Show a plain toast ui.showToast('Action completed!'); // Show a styled toast ui.showToast({ text: 'Success!', appearance: 'success' }); // Navigate to a URL ui.navigateTo('https://reddit.com/r/devvit'); // Navigate to a Reddit object const sub = await reddit.getCurrentSubreddit(); ui.navigateTo(sub); // Show a registered form // ui.showForm(myFormKey, { prefillData: 'value' }); // Post message to a mounted web view // ui.webView.postMessage('my-webview-id', { type: 'REFRESH' }); }, }); export default Devvit; ``` -------------------------------- ### Devvit App Manifest Configuration Source: https://context7.com/reddit/devvit/llms.txt The `devvit.yaml` file is the app manifest, declaring app metadata. It must be present in every Devvit app package. ```yaml # devvit.yaml — example manifest name: my-community-app version: 0.1.0 # Optional: specify the entry point if not src/main.tsx # main: src/main.tsx ``` -------------------------------- ### useChannel() Source: https://context7.com/reddit/devvit/llms.txt Subscribes the post to a named realtime channel for real-time pub/sub messaging. Server-side code sends messages via `context.realtime.send()`. The channel name must be alphanumeric with underscores only. ```APIDOC ## useChannel() ### Description Subscribes the post to a named realtime channel for real-time pub/sub messaging. Server-side code sends messages via `context.realtime.send()`. The channel name must be alphanumeric with underscores only. ### Usage ```typescript import { useChannel } from '@devvit/public-api'; const channel = useChannel({ name: 'channel_name', onMessage: (msg) => { /* handle message */ }, onSubscribed: async () => { /* handle subscription */ }, onUnsubscribed: async () => { /* handle unsubscription */ }, }); channel.subscribe(); channel.send(message); ``` ### Parameters #### `useChannel` options - **name** (string) - Required - The name of the realtime channel. - **onMessage** ((msg: MessageType) => void) - Optional - Callback function to handle incoming messages. - **onSubscribed** (async () => void) - Optional - Callback function when successfully subscribed. - **onUnsubscribed** (async () => void) - Optional - Callback function when unsubscribed. ### Methods - **subscribe()**: Subscribes to the channel. - **send(message: MessageType)**: Sends a message to the channel. ``` -------------------------------- ### Create Interactive Forms with Devvit.createForm() and useForm() Source: https://context7.com/reddit/devvit/llms.txt Use Devvit.createForm() for module-level forms accessible from menu items, or useForm() for inline forms within custom post types. Both return a FormKey to be used with context.ui.showForm(). ```typescript import { Devvit, useForm } from '@devvit/public-api'; Devvit.configure({ redditAPI: true }); // Module-level form (for menu items) const reportForm = Devvit.createForm( { title: 'Report Issue', description: 'Tell us what went wrong', fields: [ { name: 'category', label: 'Category', type: 'select', options: [ { label: 'Bug', value: 'bug' }, { label: 'Spam', value: 'spam' }, ], required: true }, { name: 'description', label: 'Description', type: 'paragraph', required: true }, ], acceptLabel: 'Submit', cancelLabel: 'Cancel', }, async (values, context) => { const { reddit, ui } = context; await reddit.submitComment({ id: context.postId!, text: `[Report] ${values.category}: ${values.description}` }); ui.showToast({ text: 'Report submitted!', appearance: 'success' }); } ); // Inline form inside a custom post (useForm hook) Devvit.addCustomPostType({ name: 'Guest Book', render: (context) => { const signForm = useForm( { title: 'Sign the Guest Book', fields: [{ name: 'message', label: 'Your message', type: 'string', required: true }], }, async (values) => { await context.redis.rPush('guestbook', values.message as string); context.ui.showToast('Signed!'); } ); return ( ); }, }); export default Devvit; ``` -------------------------------- ### Schedule and Manage Jobs with Scheduler Source: https://context7.com/reddit/devvit/llms.txt Defines a scheduler job for posting announcements and demonstrates scheduling one-time and recurring jobs. Includes listing and canceling jobs. Requires `redditAPI` for posting. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redditAPI: true }); Devvit.addSchedulerJob({ name: 'post_announcement', onRun: async (event, { reddit }) => { const { subredditName, title, body } = event.data as { subredditName: string; title: string; body: string; }; await reddit.submitPost({ subredditName, title, text: body }); console.log(`Announcement posted to r/${subredditName}`); }, }); Devvit.addMenuItem({ label: 'Schedule Announcement', location: 'subreddit', onPress: async (_, { scheduler, reddit, ui }) => { const sub = await reddit.getCurrentSubreddit(); // One-time job: in 10 minutes const jobId = await scheduler.runJob({ name: 'post_announcement', runAt: new Date(Date.now() + 10 * 60 * 1000), data: { subredditName: sub.name, title: 'Announcement', body: 'Hello r/community!' }, }); // Recurring cron job await scheduler.runJob({ name: 'post_announcement', cron: '0 9 * * 1', // Every Monday at 9am UTC data: { subredditName: sub.name, title: 'Weekly Thread', body: 'New week!' }, }); // List and cancel jobs const jobs = await scheduler.listJobs(); console.log(`${jobs.length} scheduled jobs`); await scheduler.cancelJob(jobId); ui.showToast(`Announcement scheduled (${jobId})`); }, }); export default Devvit; ``` -------------------------------- ### Redis Client Operations with `context.redis` Source: https://context7.com/reddit/devvit/llms.txt Interact with a Redis-compatible client for strings, hashes, sorted sets, and transactions. Supports key expiration and global keys via `redis.global`. Ensure `redis: true` is configured. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redis: true }); Devvit.addTrigger({ event: 'PostSubmit', async onEvent(event, { redis }) { const postId = event.post!.id; const authorId = event.author!.id; // String operations await redis.set(`post:${postId}:score`, '0'); await redis.incrBy(`post:${postId}:score`, 10); const score = await redis.get(`post:${postId}:score`); // Hash operations await redis.hSet(`user:${authorId}`, { posts: '1', lastPost: postId }); const userData = await redis.hGetAll(`user:${authorId}`); // Sorted set for leaderboard await redis.zAdd('leaderboard', { score: 100, member: authorId }); const top10 = await redis.zRange('leaderboard', 0, 9, { by: 'rank', reverse: true }); // Key expiry await redis.expire(`post:${postId}:score`, 3600); // expires in 1 hour // Atomic transaction const tx = await redis.watch(`user:${authorId}`); await tx.multi(); await tx.hSet(`user:${authorId}`, { posts: String(Number(userData.posts ?? 0) + 1) }); const results = await tx.exec(); console.log({ score, userData, top10, results }); }, }); export default Devvit; ``` -------------------------------- ### Bundle Devvit Web View Script Source: https://github.com/reddit/devvit/blob/main/packages/web-view-scripts/README.md Bundle the Devvit web view script using the provided command. This command is used during the development process when overriding Chrome content. ```bash yarn workspace @devvit/web-view-scripts bundle --outfile=dist/scripts/devvit.v1.min.js src/devvit.v1.ts ``` -------------------------------- ### Configure Devvit Platform Capabilities Source: https://context7.com/reddit/devvit/llms.txt Enables specific Reddit platform plugins like Reddit API, Redis, realtime messaging, media uploads, and HTTP requests. Configure allowed domains for HTTP requests and scopes for user actions. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redditAPI: true, // Enables context.reddit client (posts, comments, users, etc.) redis: true, // Enables context.redis client (default: on) realtime: true, // Enables context.realtime and useChannel hook media: true, // Enables context.media.upload() http: { domains: ['api.openai.com', 'api.weather.gov'], // Allowlisted fetch domains (max 10) }, userActions: { scopes: ['submit_post', 'submit_comment'], // On-behalf-of-user actions }, }); ``` -------------------------------- ### devvit.yaml Source: https://context7.com/reddit/devvit/llms.txt The app manifest file that declares app metadata. It must be present in every Devvit app package. ```APIDOC ## `devvit.yaml` — App manifest configuration The manifest file declares app metadata and must be present in every Devvit app package. ### Fields - `name` (string) - Required - The name of the application. - `version` (string) - Required - The version of the application. - `main` (string) - Optional - Specifies the entry point file if it's not `src/main.tsx`. ### Example ```yaml # devvit.yaml — example manifest name: my-community-app version: 0.1.0 # Optional: specify the entry point if not src/main.tsx # main: src/main.tsx ``` ``` -------------------------------- ### Real-time Pub/Sub Messaging with `useChannel()` Source: https://context7.com/reddit/devvit/llms.txt Subscribe to a named channel for real-time messages. Server-side code sends messages using `context.realtime.send()`. The channel name must be alphanumeric with underscores only. Ensure `realtime: true` is configured. ```typescript import { Devvit, useState, useChannel } from '@devvit/public-api'; import { ChannelStatus } from '@devvit/public-api/types/realtime.js'; Devvit.configure({ realtime: true, redditAPI: true }); type LiveMessage = { author: string; text: string }; // Server sends messages via a scheduler job Devvit.addSchedulerJob({ name: 'broadcast', onRun: async (event, { realtime }) => { await realtime.send('live_chat', { author: 'bot', text: 'Hello everyone!' }); }, }); Devvit.addCustomPostType({ name: 'Live Chat', render: (context) => { const [messages, setMessages] = useState([]); const channel = useChannel({ name: 'live_chat', onMessage: (msg) => { setMessages((prev) => [...(prev ?? []), msg]); }, onSubscribed: async () => console.log('Connected to live chat'), onUnsubscribed: async () => console.log('Disconnected'), }); channel.subscribe(); const statusEmoji = channel.status === ChannelStatus.Connected ? '🟢' : channel.status === ChannelStatus.Connecting ? '🟡' : '🔴'; return ( Live Chat {statusEmoji} {messages.map((m, i) => ( {m.author}: {m.text} ))} ); }, }); export default Devvit; ``` -------------------------------- ### Rebuild Devvit CLI After Changes Source: https://github.com/reddit/devvit/blob/main/packages/cli/README.md After making changes to the CLI code, run this command to rebuild the project. For changes outside the CLI package, a full repository build is required. ```bash yarn build ``` -------------------------------- ### Include Devvit Web View Script Source: https://github.com/reddit/devvit/blob/main/packages/web-view-scripts/README.md Include this script tag in your HTML files to load the Devvit web view script from the CDN. Specify the client version as needed. ```html ``` -------------------------------- ### Devvit.addMenuItem() Source: https://context7.com/reddit/devvit/llms.txt Adds a menu item visible in the Reddit interface, which can be triggered by user actions. ```APIDOC ## Devvit.addMenuItem() ### Description Adds a menu item visible in the Reddit interface. The `location` determines where the item appears: `'post'`, `'comment'`, `'subreddit'`, or an array of those. ### Method `Devvit.addMenuItem(options: object)` ### Parameters #### Options Object - **label** (string) - Required - The text displayed for the menu item. - **location** (string | string[]) - Required - Where the menu item appears (e.g., 'post', 'comment', 'subreddit'). - **forUserType** (string) - Optional - Restricts visibility to specific user types (e.g., 'moderator'). - **onPress** (function) - Required - Callback function executed when the menu item is pressed. - **event** (object) - Event object passed to the callback. - **context** (object) - The context object provided to the callback. - **reddit** (object) - Reddit API client. - **ui** (object) - UI manipulation functions. ### Request Example ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redditAPI: true }); Devvit.addMenuItem({ label: 'Create Weekly Thread', location: 'subreddit', forUserType: 'moderator', onPress: async (event, context) => { const { reddit, ui } = context; try { const sub = await reddit.getCurrentSubreddit(); const post = await reddit.submitPost({ subredditName: sub.name, title: `Weekly Discussion — ${new Date().toDateString()}`, text: 'Welcome to this week\'s discussion thread!', }); ui.navigateTo(post); ui.showToast({ text: 'Thread created!', appearance: 'success' }); } catch (e) { ui.showToast('Failed to create thread.'); } }, }); export default Devvit; ``` ``` -------------------------------- ### Disable Script Injection in Client Entrypoint Source: https://github.com/reddit/devvit/blob/main/packages/web-view-scripts/README.md Add the @devvit/web-view-scripts import to your client entrypoint before any other code to disable script injection. This is useful for local playtesting. ```typescript // src/client/index.ts import '@devvit/web-view-scripts/scripts/devvit.v1.min.js'; import { context } from '@devvit/web/client'; ``` -------------------------------- ### Devvit.addSchedulerJob() Source: https://context7.com/reddit/devvit/llms.txt Registers a named job that can be triggered on-demand or on a cron schedule via context.scheduler.runJob(). Jobs receive their typed data payload and the full trigger context. ```APIDOC ## Devvit.addSchedulerJob() ### Description Registers a named job that can be triggered on-demand or on a cron schedule via `context.scheduler.runJob()`. Jobs receive their typed `data` payload and the full trigger context. ### Usage ```typescript Devvit.addSchedulerJob({ name: 'job_name', onRun: async (event, context) => { // Job logic here }, }); ``` ### Example Usage with Triggers and Menu Items ```typescript // Schedule on install with cron Devvit.addTrigger({ event: 'AppInstall', async onEvent(event, { scheduler }) { await scheduler.runJob({ name: 'cleanup_old_data', cron: '0 0 * * *', // Example cron schedule data: { subredditName: event.subreddit?.name ?? '' }, }); }, }); // Schedule from a menu item Devvit.addMenuItem({ label: 'Run job now', location: 'subreddit', onPress: async (_, { scheduler, ui, reddit }) => { const sub = await reddit.getCurrentSubreddit(); const jobId = await scheduler.runJob({ name: 'cleanup_old_data', runAt: new Date(Date.now() + 5 * 60 * 1000), // 5 minutes from now data: { subredditName: sub.name }, }); ui.showToast(`Job scheduled (job: ${jobId})`); }, }); ``` ``` -------------------------------- ### Playtest Command with Script Injection Disabled Source: https://github.com/reddit/devvit/blob/main/packages/web-view-scripts/README.md Use this command to playtest your Devvit application with web view script injection disabled. Ensure you have linked your local packages correctly. ```bash $ cd devvit $ yarn build $ cd ~/some-test-app $ npm link --save ~/work/reddit/src/devvit/packages/{devvit,public-api,web,web-view-scripts} $ DEVVIT_SKIP_WEB_VIEW_SCRIPT_INJECTION=1 npx devvit playtest r/some-test-sub ``` -------------------------------- ### Register Background Job Handlers with Devvit.addSchedulerJob() Source: https://context7.com/reddit/devvit/llms.txt Use Devvit.addSchedulerJob() to register named background jobs. These jobs can be triggered on-demand or via a cron schedule using context.scheduler.runJob(). Jobs receive a typed data payload and the full trigger context. ```typescript import { Devvit } from '@devvit/public-api'; Devvit.configure({ redditAPI: true, redis: true }); Devvit.addSchedulerJob({ name: 'cleanup_old_data', onRun: async (event, context) => { const { redis, reddit } = context; const { subredditName } = event.data as { subredditName: string }; const keys = await redis.hKeys(`stale:${subredditName}`); if (keys.length) await redis.del(`stale:${subredditName}`); console.log(`Cleaned ${keys.length} keys for r/${subredditName}`); }, }); // Schedule on install with cron Devvit.addTrigger({ event: 'AppInstall', async onEvent(event, { scheduler }) { await scheduler.runJob({ name: 'cleanup_old_data', cron: '0 0 * * 0', // Every Sunday at midnight data: { subredditName: event.subreddit?.name ?? '' }, }); }, }); // Schedule from a menu item (one-time, 5 min from now) Devvit.addMenuItem({ label: 'Run cleanup now', location: 'subreddit', onPress: async (_, { scheduler, ui, reddit }) => { const sub = await reddit.getCurrentSubreddit(); const jobId = await scheduler.runJob({ name: 'cleanup_old_data', runAt: new Date(Date.now() + 5 * 60 * 1000), data: { subredditName: sub.name }, }); ui.showToast(`Cleanup scheduled (job: ${jobId})`); }, }); export default Devvit; ``` -------------------------------- ### useState for Reactive State in Custom Posts Source: https://context7.com/reddit/devvit/llms.txt Use `useState` to manage reactive state within custom post renders. It supports synchronous initial values or asynchronous initializers, which trigger a loading state until resolved. State must be JSON-serializable. ```typescript import { Devvit, useState } from '@devvit/public-api'; Devvit.configure({ redis: true }); Devvit.addCustomPostType({ name: 'Scoreboard', render: (context) => { // Async initializer: fetches from Redis, shows loading state while pending const [scores, setScores] = useState<{ user: string; score: number }[]>(async () => { const raw = await context.redis.get('scores'); return raw ? JSON.parse(raw) : []; }); const addScore = async () => { const username = await context.reddit.getCurrentUsername(); if (!username) return; const next = [...scores, { user: username, score: Date.now() % 100 }]; await context.redis.set('scores', JSON.stringify(next)); setScores(next); }; return ( Scoreboard {scores.map((s) => ( {s.user} {String(s.score)} ))} ); }, }); export default Devvit; ```