import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem';
Configure your app with settings that can be customized per subreddit or globally across all installations. Settings allow moderators to customize app behavior for their subreddit, while secrets enable secure storage of sensitive data like API keys.
Settings come in two scopes:
- Subreddit settings: Configurable by moderators for each installation
- Global settings & Secrets: Set by developers and shared across all installations
:::warning
Local environment variables and .env files are read during playtesting only.
:::
Settings are defined differently depending on whether you're using Devvit Web or Devvit Blocks.
Define settings in your `devvit.json` file under the `settings` object. Settings are organized by scope: `global` for app-wide settings and secrets, `subreddit` for installation-specific settings.{
"settings": {
"global": {
"apiKey": {
"type": "string",
"label": "API Key",
"defaultValue": "",
"isSecret": true
},
"environment": {
"type": "select",
"label": "Environment",
"options": [
{
"label": "Production",
"value": "production"
},
{
"label": "Development",
"value": "development"
}
],
"defaultValue": "production"
}
},
"subreddit": {
"welcomeMessage": {
"type": "string",
"label": "Welcome Message",
"validationEndpoint": "/internal/settings/validate-message",
"defaultValue": "Welcome to our community!"
},
"enabledFeatures": {
"type": "multiSelect",
"label": "Enabled Features",
"options": [
{
"label": "Auto-moderation",
"value": "automod"
},
{
"label": "Welcome posts",
"value": "welcome"
},
{
"label": "Statistics tracking",
"value": "stats"
}
],
"defaultValue": ["welcome"]
}
}
}
}:::note
After defining settings in devvit.json, you must build your app (npm run dev) before you can set secrets via the CLI.
:::
Use Devvit.addSettings to define settings with the appropriate scope.
import { Devvit, SettingScope } from '@devvit/public-api';
Devvit.addSettings([
// Global secret
{
type: 'string',
name: 'apiKey',
label: 'API Key',
isSecret: true,
scope: SettingScope.App, // or 'app'
},
// Global setting
{
type: 'select',
name: 'environment',
label: 'Environment',
options: [
{ label: 'Production', value: 'production' },
{ label: 'Development', value: 'development' },
],
scope: 'app',
},
// Subreddit setting
{
type: 'string',
name: 'welcomeMessage',
label: 'Welcome Message',
scope: SettingScope.Installation, // or 'installation'
onValidate: async ({ value }) => {
if (!value || value.length < 5) {
return 'Message must be at least 5 characters';
}
},
},
// Subreddit multi-select
{
type: 'select',
name: 'enabledFeatures',
label: 'Enabled Features',
options: [
{ label: 'Auto-moderation', value: 'automod' },
{ label: 'Welcome posts', value: 'welcome' },
{ label: 'Statistics tracking', value: 'stats' },
],
multiSelect: true,
scope: 'installation',
},
]);Both frameworks support the following setting types:
- string: Text input field
- boolean: Toggle switch
- number: Numeric input
- select: Dropdown selection (single choice)
- multiSelect (Web) / select with multiSelect: true (Blocks): Multiple choice dropdown
- paragraph: Multi-line text input (Blocks only)
- group: Grouped settings for organization (Blocks only)
Secrets are global settings marked with isSecret: true. They're encrypted and can only be set by developers via the CLI.
View all defined secrets in your app:
npx devvit settings list
Key Label Is this a secret? Type
────────── ─────────── ───────────────── ──────
apiKey API Key true STRING
environment Environment false SELECTOnly app developers can set secret values:
npx devvit settings set apiKey
? Enter the value you would like to assign to the variable apiKey: <value>
Updating app settings... ✅
Successfully added app settings for apiKey!:::warning
At least one app installation is required before you can store secrets via the CLI. You can run npx devvit playtest (or npm run dev in Devvit Web) to start your first installation.
:::
Settings can be retrieved from within your app.
import { settings } from '@devvit/web/server';
type ProcessResponse = { success: true };
// Get a single setting
const apiKey = await settings.get('apiKey');
// Get multiple settings
const [welcomeMessage, features] = await Promise.all([
settings.get('welcomeMessage'),
settings.get('enabledFeatures')
]);
// Use in an endpoint
app.post('/api/process', async (c) => {
const apiKey = await settings.get('apiKey');
const environment = await settings.get('environment');
const response = await fetch('https://api.example.com/endpoint', {
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-Environment': environment
}
});
return c.json<ProcessResponse>({ success: true });
});import { settings } from '@devvit/web/server';
type ProcessResponse = { success: true };
// Get a single setting
const apiKey = await settings.get('apiKey');
// Get multiple settings
const [welcomeMessage, features] = await Promise.all([
settings.get('welcomeMessage'),
settings.get('enabledFeatures')
]);
// Use in an endpoint
router.post<string, never, ProcessResponse, never>('/api/process', async (req, res) => {
const apiKey = await settings.get('apiKey');
const environment = await settings.get('environment');
const response = await fetch('https://api.example.com/endpoint', {
headers: {
'Authorization': `Bearer ${apiKey}`,
'X-Environment': environment
}
});
res.json({ success: true });
});Devvit.addMenuItem({ label: 'Process with API', location: 'subreddit', onPress: async (_event, context) => { // Get individual settings const apiKey = await context.settings.get('apiKey'); const welcomeMessage = await context.settings.get('welcomeMessage');
// Get all settings
const allSettings = await context.settings.getAll();
// Use the settings
const response = await fetch('https://api.example.com/endpoint', {
headers: {
'Authorization': `Bearer ${apiKey}`,
}
});
context.ui.showToast(`Processed with message: ${welcomeMessage}`);
},
});
// Access in custom post type // addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. View the announcement below this example. Devvit.addCustomPostType({ name: 'ConfigurablePost', render: (context) => { const [features, setFeatures] = useState<string[]>([]);
useEffect(async () => {
const enabledFeatures = await context.settings.get('enabledFeatures');
setFeatures(enabledFeatures || []);
}, []);
return (
<vstack>
<text>Enabled features: {features.join(', ')}</text>
</vstack>
);
},
});
[View `addCustomPostType` deprecation announcement.](https://www.reddit.com/r/Devvit/comments/1r3xcm2/devvit_web_and_the_future_of_devvit/)
</TabItem>
</Tabs>
## Input validation
Validate user input to ensure it meets your requirements before saving.
<Tabs>
<TabItem value="web" label="Devvit Web">
Define a validation endpoint in your `devvit.json` and implement it in your server:
```json title="devvit.json"
{
"settings": {
"subreddit": {
"minimumAge": {
"type": "number",
"label": "Minimum Account Age (days)",
"validationEndpoint": "/internal/settings/validate-age",
"defaultValue": 7
}
}
}
}
<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { SettingsValidationRequest, SettingsValidationResponse } from '@devvit/web/shared';
app.post('/internal/settings/validate-age', async (c) => {
const { value } = await c.req.json<SettingsValidationRequest<number>>();
if (!value || value < 0) {
return c.json<SettingsValidationResponse>({
success: false,
error: 'Age must be a positive number',
});
}
if (value > 365) {
return c.json<SettingsValidationResponse>({
success: false,
error: 'Maximum age is 365 days',
});
}
return c.json<SettingsValidationResponse>({ success: true });
});import type { SettingsValidationRequest, SettingsValidationResponse } from '@devvit/web/shared';
router.post<string, never, SettingsValidationResponse, SettingsValidationRequest<number>>(
'/internal/settings/validate-age',
async (req, res): Promise<void> => {
const { value } = req.body;
if (!value || value < 0) {
res.json({
success: false,
error: 'Age must be a positive number',
});
return;
}
if (value > 365) {
res.json({
success: false,
error: 'Maximum age is 365 days',
});
return;
}
res.json({ success: true });
}
);Devvit.addSettings([
{
type: 'number',
name: 'minimumAge',
label: 'Minimum Account Age (days)',
scope: 'installation',
onValidate: async ({ value }) => {
if (!value || value < 0) {
return 'Age must be a positive number';
}
if (value > 365) {
return 'Maximum age is 365 days';
}
},
},
]);Once your app is installed, moderators can configure subreddit settings through the Install Settings page. These settings are scoped to the specific subreddit where the app is installed.
Moderators will see all non-secret settings defined for the subreddit scope and can update them as needed. Changes are saved immediately and available to your app.
Here's a complete example showing both secrets and subreddit settings in action:
```json title="devvit.json" { "settings": { "global": { "openaiApiKey": { "type": "string", "label": "OpenAI API Key", "isSecret": true, "defaultValue": "" } }, "subreddit": { "aiModel": { "type": "select", "label": "AI Model", "options": [ { "label": "GPT-4", "value": "gpt-4" }, { "label": "GPT-3.5", "value": "gpt-3.5-turbo" } ], "defaultValue": "gpt-3.5-turbo" }, "maxTokens": { "type": "number", "label": "Max Response Tokens", "validationEndpoint": "/internal/settings/validate-tokens", "defaultValue": 150 } } } } ```<Tabs variant="pill" groupId="http-server-framework" defaultValue="hono" values={[ { label: 'Hono', value: 'hono' }, { label: 'Express', value: 'express' }, ]}>
import type { JsonObject, JsonValue } from '@devvit/web/shared';
import { settings } from '@devvit/web/server';
type GenerateRequest = { messages: JsonValue };
type GenerateResponse = JsonObject;
app.post('/api/generate', async (c) => {
const [apiKey, model, maxTokens] = await Promise.all([
settings.get('openaiApiKey'),
settings.get('aiModel'),
settings.get('maxTokens')
]);
const { messages } = await c.req.json<GenerateRequest>();
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
max_tokens: maxTokens,
messages,
}),
});
const data = (await response.json()) as GenerateResponse;
return c.json<GenerateResponse>(data);
});import type { JsonObject, JsonValue } from '@devvit/web/shared';
import { settings } from '@devvit/web/server';
type GenerateRequest = { messages: JsonValue };
type GenerateResponse = JsonObject;
router.post<string, never, GenerateResponse, GenerateRequest>('/api/generate', async (req, res) => {
const [apiKey, model, maxTokens] = await Promise.all([
settings.get('openaiApiKey'),
settings.get('aiModel'),
settings.get('maxTokens')
]);
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
max_tokens: maxTokens,
messages: req.body.messages,
}),
});
const data = (await response.json()) as GenerateResponse;
res.json(data);
});Devvit.configure({ http: true, });
Devvit.addSettings([ { name: 'openaiApiKey', label: 'OpenAI API Key', type: 'string', isSecret: true, scope: 'app', }, { name: 'aiModel', label: 'AI Model', type: 'select', options: [ { label: 'GPT-4', value: 'gpt-4' }, { label: 'GPT-3.5', value: 'gpt-3.5-turbo' }, ], scope: 'installation', }, { name: 'maxTokens', label: 'Max Response Tokens', type: 'number', scope: 'installation', onValidate: async ({ value }) => { if (value < 1 || value > 500) { return 'Tokens must be between 1 and 500'; } }, }, ]);
async function generateResponse(context: Devvit.Context, prompt: string): Promise { const [apiKey, model, maxTokens] = await Promise.all([ context.settings.get('openaiApiKey'), context.settings.get('aiModel'), context.settings.get('maxTokens'), ]);
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
max_tokens: maxTokens,
messages: [{ role: 'user', content: prompt }],
}),
});
const data = await response.json();
return data.choices[0]?.message?.content || 'No response';
}
// addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. View the announcement below this example. Devvit.addCustomPostType({ name: 'AI Assistant', render: (context) => { const [response, setResponse] = useState('');
return (
<vstack gap="medium" padding="medium">
<button
onPress={async () => {
const result = await generateResponse(context, 'Hello!');
setResponse(result);
}}
>
Generate Response
</button>
{response && <text>{response}</text>}
</vstack>
);
},
});
export default Devvit;
[View `addCustomPostType` deprecation announcement.](https://www.reddit.com/r/Devvit/comments/1r3xcm2/devvit_web_and_the_future_of_devvit/)
</TabItem>
</Tabs>
## Limitations
- Secrets can only be global
- Secrets can only be set via CLI by app developers
- Setting values are currently not fully surfaced in the CLI
- Maximum of 2KB per setting value