wp-config.php
Database constants, salts, the table prefix and the debug and environment flags — the one file that configures a WordPress install.
define( 'WP_ENVIRONMENT_TYPE', 'staging' ); // local | development | staging | productionwp-config.php is the first file of yours that WordPress loads. wp-load.php requires it, it defines constants and the $table_prefix variable, then it requires wp-settings.php — which loads the API, the plugins and the theme. Everything you add must therefore go above that require, conventionally above the /* That's all, stop editing! */ comment. The file is not part of the release archive, so updates never touch it.
What lives in it
- Database:
DB_NAME,DB_USER,DB_PASSWORD,DB_HOST,DB_CHARSET,DB_COLLATE. - Salts: eight values —
AUTH_KEY,SECURE_AUTH_KEY,LOGGED_IN_KEY,NONCE_KEYand the matching_SALTconstants. They key the auth cookies and nonces; changing any of them logs every user out. - Table prefix:
$table_prefix, a variable rather than a constant. Tables are created with it at install time. - Debugging:
WP_DEBUG,WP_DEBUG_LOG(true, or a file path),WP_DEBUG_DISPLAY,SCRIPT_DEBUG,SAVEQUERIES. - Environment:
WP_ENVIRONMENT_TYPE(WordPress 5.5 —local,development,staging,production, read back withwp_get_environment_type()) andWP_DEVELOPMENT_MODE(6.3 —core,plugin,theme,all). - Lockdown:
DISALLOW_FILE_EDITremoves the admin file editors;DISALLOW_FILE_MODSblocks all installs and updates from the admin.
What trips people up
Core's own functions do not exist yet at this point in the bootstrap, so you cannot call wp_get_environment_type() or get_option() here. Branch on a plain variable or an environment variable instead.
Example
<?php
/**
* wp-config.php — everything below must stay ABOVE the
* require_once ABSPATH . 'wp-settings.php'; line at the bottom.
*/
// Core functions are not loaded yet: wp_get_environment_type(), get_option()
// and the hooks API do not exist here. Branch on your own variable.
$acme_env = getenv( 'ACME_ENV' ) ?: 'production';
// ** Database ** //
define( 'DB_NAME', getenv( 'ACME_DB_NAME' ) );
define( 'DB_USER', getenv( 'ACME_DB_USER' ) );
define( 'DB_PASSWORD', getenv( 'ACME_DB_PASSWORD' ) ); // Secrets out of the repo.
define( 'DB_HOST', '127.0.0.1' );
define( 'DB_CHARSET', 'utf8mb4' ); // The shipped sample still says 'utf8'.
define( 'DB_COLLATE', '' );
// ** Salts — eight of them, from api.wordpress.org/secret-key/1.1/salt/ ** //
define( 'AUTH_KEY', getenv( 'ACME_AUTH_KEY' ) );
define( 'SECURE_AUTH_KEY', getenv( 'ACME_SECURE_AUTH_KEY' ) );
define( 'LOGGED_IN_KEY', getenv( 'ACME_LOGGED_IN_KEY' ) );
define( 'NONCE_KEY', getenv( 'ACME_NONCE_KEY' ) );
define( 'AUTH_SALT', getenv( 'ACME_AUTH_SALT' ) );
define( 'SECURE_AUTH_SALT', getenv( 'ACME_SECURE_AUTH_SALT' ) );
define( 'LOGGED_IN_SALT', getenv( 'ACME_LOGGED_IN_SALT' ) );
define( 'NONCE_SALT', getenv( 'ACME_NONCE_SALT' ) );
// ** Environment ** //
define( 'WP_ENVIRONMENT_TYPE', $acme_env );
// ** Debugging: log everywhere, display nowhere ** //
define( 'WP_DEBUG', 'production' !== $acme_env );
define( 'WP_DEBUG_DISPLAY', false ); // Never print errors to visitors.
define( 'WP_DEBUG_LOG', __DIR__ . '/wp-content/logs/debug.log' ); // dir must exist
define( 'SCRIPT_DEBUG', 'local' === $acme_env );
// ** Hard-coded URLs beat whatever is in the options table ** //
define( 'WP_HOME', getenv( 'ACME_URL' ) ); // No trailing slash.
define( 'WP_SITEURL', getenv( 'ACME_URL' ) );
define( 'FORCE_SSL_ADMIN', true );
// ** Lockdown: code ships from the repo, not from the admin ** //
define( 'DISALLOW_FILE_EDIT', true ); // Hides the theme and plugin file editors.
define( 'DISALLOW_FILE_MODS', 'production' === $acme_env ); // No installs/updates.
// A variable, not a constant. Changing it after install makes WordPress
// believe the site has never been installed.
$table_prefix = 'acme_';
/* That's all, stop editing! Happy publishing. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
require_once ABSPATH . 'wp-settings.php';When to use it
- Setting up per-environment debug and mail behaviour for local, staging and production.
- Locking a production site so code can only arrive through a deployment.
- Rotating salts after a suspected session hijack.
- Overriding a site URL to recover an install after a domain move.
More examples
The guard clause, short and long
Both forms appear constantly in real plugins, and the shorthand is opaque the first time you meet it. The conditional define is the pattern to use in wp-config.php when a constant may already have been set by a hosting platform's own drop-in configuration.
<?php
// At the top of nearly every plugin and template file you will see this:
defined( 'ABSPATH' ) || exit;
// It is shorthand for exactly this, using PHP's short-circuiting ||:
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// The same idiom is used to define a constant only if nobody else has:
defined( 'ACME_VERSION' ) || define( 'ACME_VERSION', '1.4.0' );
// ...which is the shorthand for:
if ( ! defined( 'ACME_VERSION' ) ) {
define( 'ACME_VERSION', '1.4.0' );
}
// Note the difference between the two functions, which reads confusingly:
// defined( 'X' ) — has the constant X been declared? (string argument)
// define( 'X', 1 ) — declare it.Reading the environment after the bootstrap
The safety net every multi-environment WordPress project needs. Set WP_ENVIRONMENT_TYPE once in wp-config.php per environment, then branch on it in code rather than sprinkling hostname checks through the codebase.
<?php
// In a plugin or mu-plugin — by now core IS loaded, so use the API.
add_action( 'plugins_loaded', static function () {
// wp_get_environment_type() (WordPress 5.5) returns one of:
// 'local', 'development', 'staging', 'production'. Default 'production'.
if ( 'production' === wp_get_environment_type() ) {
return;
}
// Never let a staging copy email real customers.
add_filter( 'wp_mail', static function ( array $args ): array {
$args['to'] = '[email protected]';
$args['subject'] = '[' . wp_get_environment_type() . '] ' . $args['subject'];
return $args;
} );
add_filter( 'pre_option_blog_public', '__return_zero' ); // Discourage indexing.
} );
// wp_get_development_mode() (WordPress 6.3) is separate: 'core', 'plugin',
// 'theme', 'all' or ''. It relaxes caching of things like theme.json.
if ( function_exists( 'wp_get_development_mode' ) && 'theme' === wp_get_development_mode() ) {
// theme.json is re-read on every request instead of being cached.
}Rotating salts and prefix realities
Salt rotation is the correct response to a suspected session-token leak and takes one command. The prefix notes are here because 'change the table prefix for security' is common bad advice that breaks sites: the prefix is stored inside option and usermeta keys too.
# Regenerate the eight key/salt constants in place. Every logged-in user is
# signed out immediately, and pending nonces stop validating.
wp config shuffle-salts
# Shuffle only specific keys.
wp config shuffle-salts NONCE_KEY NONCE_SALT
# Inspect what the running site thinks its config is.
wp config get --constant=WP_DEBUG
wp config list --strict
# The table prefix is NOT safe to change after install. These tables exist:
wp db tables
# acme_posts, acme_postmeta, acme_options, acme_users, ...
# Editing $table_prefix alone points WordPress at tables that do not exist,
# so it redirects to install.php. A real rename means renaming every table AND
# rewriting the prefixed rows in wp_options (*_user_roles) and wp_usermeta
# (*_capabilities, *_user_level).
Discussion