Skip to content

laravel-env-editor

innoboxrr/laravel-env-editor 2.1.1 lets you edit the application's .env without logging into the server. You can add, change and delete keys, upload another .env, and create or restore backups. It works from a web UI or through the EnvEditor facade, without breaking the file's structure.

Whoever reaches this controls the application

The editor reads and rewrites the whole .env, including APP_KEY and every credential. Only enable it behind an admin check.

Install

bash
composer require innoboxrr/laravel-env-editor
php artisan vendor:publish --provider="Innoboxrr\EnvEditor\ServiceProvider" --tag=config

It requires PHP ^8.3 and laravel/framework ^13.0. The provider and the EnvEditor alias are auto-discovered. The UI ships disabled; enable it in config/env-editor.php.

Configuration

config/env-editor.php:

KeyDefaultWhat it decides
paths.backupDirectorystorage_path('env-editor')Where .env backups are stored
route.enablefalseRegister the UI routes
route.prefixenv-editorURI prefix
route.nameenv-editorRoute name prefix
route.middleware['web', 'auth']Group middleware. auth is the minimum; add your own admin check
route.gatenullAbility required on every action, on top of the middleware
timeFormatd/m/Y H:i:sDate format in the UI and backups
layoutenv-editor::layoutThe view index.blade.php extends
php
'route' => [
    'enable' => true,
    'middleware' => ['web', 'auth', 'admin'],
    'gate' => 'manage-env',
],

Environment variables

The package reads no environment variables.

ENV_EDITOR_ENABLED belongs to the base application

ENV_EDITOR_ENABLED doesn't exist in the package or its README. It is defined by the config/env-editor.php that laravel-setup copies, as 'enable' => env('ENV_EDITOR_ENABLED', true).

Publishing

All three tags are generic, so always pass --provider="Innoboxrr\EnvEditor\ServiceProvider".

TagWhat it copies
configconfig/env-editor.php
viewsresources/views/vendor/env-editor
translationsresources/lang/vendor/env-editor

Migrations

It ships no migrations.

Commands

It registers no commands. Saving the .env schedules the OptimizeApplication job (php artisan optimize) one minute later, only when the configuration is cached. Without a config cache, new values are read on the next request.

HTTP routes

They only exist when route.enable is true. They live under /env-editor, with the route.middleware middleware and env-editor.* names.

MethodURINameFieldsResponse
GET/env-editorenv-editor.indexThe UI, or { items, success } with JSON
POST/env-editor/keyenv-editor.keykey, value, and optionally index or group to position it{ message, success }
PATCH/env-editor/keykey, value{ message, success }
DELETE/env-editor/keykey{ message, success }
DELETE/env-editor/clear-cacheenv-editor.clearConfigCacheRuns config:clear; { message, success }
GET/env-editor/filesenv-editor.getBackupsThe UI, or { items, success } with JSON
POST/env-editor/files/create-backupenv-editor.createBackup{ message, success }
POST/env-editor/files/restore-backup/{filename?}env-editor.restoreBackup{ message, success }
DELETE/env-editor/files/destroy-backup/{filename?}env-editor.destroyBackup{ message, success }
GET/env-editor/files/download/{filename?}env-editor.downloadDownloads the backup, or the current .env when no name is given
POST/env-editor/files/uploadenv-editor.uploadfile (text), replace_current (boolean)Stores it as a backup, or replaces the .env
  • Errors. Responses are 200 with success: true, or 400 with success: false. An editor error on a JSON request comes back as 400 with { message, success: false }.
  • Backup names. A name that escapes the backup directory is rejected. Without a name, restore-backup and destroy-backup answer 400.
  • Unnamed routes. The PATCH and DELETE key routes have no name, so they don't appear in routes.json.

Policies and gates

It defines no policies. With route.gate set, the controller requires can:<ability> on every action, after the middleware: a guest gets 401 (or the redirect to login) and a user without the ability gets 403.

php
// App\Providers\AppServiceProvider::boot()
Gate::define('manage-env', fn (User $user): bool => $user->isAdmin());

Extension points

The Innoboxrr\EnvEditor\Facades\EnvEditor facade:

php
EnvEditor::getEnvFileContent($fileName = '');   // Collection; with a name, reads that backup
EnvEditor::keyExists($key);
EnvEditor::getKey($key, $default = null);
EnvEditor::addKey($key, $value, ['index' => 3]); // or ['group' => 'MAIL']
EnvEditor::editKey($key, $value);
EnvEditor::deleteKey($key);
EnvEditor::getAllBackUps();                      // Collection with date and content
EnvEditor::upload($uploadedFile, $replaceCurrentEnv);
EnvEditor::backUpCurrent();
EnvEditor::getFilePath($fileName = '');          // without a name, the .env path
EnvEditor::deleteBackup($fileName);
EnvEditor::restoreBackUp($fileName);

To change the look, publish views or point layout at your own view.

In the base application

  • Configuration. app:setup copies its own config/env-editor.php:

    php
    'route' => [
        'enable' => env('ENV_EDITOR_ENABLED', true),
        'prefix' => 'env-editor',
        'name' => 'env-editor',
        'middleware' => ['web', 'auth', 'admin'],
        'gate' => null,
    ],

    The admin alias is EnsureUserIsAdmin, which requires isAdmin(). On the generated user, that means being listed in ADMIN_EMAILS.

  • Guests. bootstrap/app.php redirects them to /auth/login, so the "Route [login] not defined" error never shows up.

  • Menu. The "Administration" group links to /env-editor as "Environment" (t('Environment')), in a new tab:

    • in Vue the link is hardcoded in resources/vue/app/router/menu.js;
    • in React it lives in adminTools in resources/react/app/config.js.

To turn it off in production, set ENV_EDITOR_ENABLED=false and remove the menu link. The link ignores that variable and would lead to a page that no longer exists.

See The admin panel.

Upgrading

From 2.0 to 2.1

  • route.middleware changes from ['web'] to ['web', 'auth']. A configuration published earlier keeps ['web'], so add auth and your admin check by hand.
  • New route.gate option, null by default.
  • Safer backups. Backup names containing .. are rejected, and restore-backup and destroy-backup without a name answer 400 instead of 500.
  • Editor errors come back as JSON.
  • optimize is only scheduled when the configuration is cached.
  • UI assets. It loads Font Awesome Free 5.15.4 and the production build of Vue 2.7.16.

URIs, route names, the other keys, the tags and the facade are unchanged.

From 2.1.0 to 2.1.1

Font Awesome and Vue load with integrity, like Bootstrap and jQuery.

Pitfalls

  • Any signed-in user gets in. Your admin check is missing from route.middleware, or the published configuration predates 2.1.0 and only has ['web'].
  • "Route [login] not defined". A guest reached an application with no login route. Define redirectGuestsTo() in bootstrap/app.php.
  • You saved a value and the application still uses the old one. The configuration is cached and optimize hasn't run yet. Wait for the job, or use "clear cache".
  • Saving fails. The web server user can't write the .env or storage/env-editor.
  • The UI has no styling. It loads Bootstrap, jQuery, Font Awesome and Vue from a CDN, and the browser can't reach them or a CSP blocks them.
  • Backups contain secrets. storage/env-editor holds full copies of the .env. Don't expose it or commit it.