Thai APP_NAME Makes Cookies -session: Pin Before Laravel 13
The cookie name users already have is determined by a default expression that nobody ever wrote into .env. Before a major upgrade, record the actual values and pin them firmly before touching composer.
1-minute summary
- Symptom / problem
- A Thai APP_NAME makes the session cookie name come out as -session, so a major upgrade risks logging users out.
- Who should read
- Maintainers of Laravel apps that use a Thai APP_NAME.
- Fix in short
- Read the real cookie, cache prefix and redis prefix from the running app and pin them in .env before upgrading from 12 to 13.
I recently had a job upgrading Laravel from 12 to 13 for around a dozen apps under one hosting administrator, plus a billing app and another app under a different administrator. The largest app had several outstanding advisories across multiple packages, so it needed an upgrade. Before the actual run, I wrote a dry run script to check each app. At first, I was only worried about whether the code would work with 13, which was the wrong concern. The more worrying part was that the session cookie name every user already had was not written down anywhere. It was calculated from the app name, and most of my apps had Thai names.
The cookie name comes from APP_NAME, without anyone telling you
In Laravel's config, the defaults for three things used to keep apps separate come from the same expression: pass APP_NAME through Str::slug, then append a suffix. I found them here in the Laravel 13 skeleton.
// config/session.php
'cookie' => env('SESSION_COOKIE', Str::slug((string) env('APP_NAME', 'laravel')).'-session'),
// config/cache.php
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
// config/database.php (redis options)
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
When APP_NAME is Thai, Str::slug cannot transliterate the Thai characters, so the result is an empty string. The actual names in use become -session, -cache- and -database-. They work normally, with no errors or warnings, and nobody had noticed all this time.
Why this is dangerous during a major upgrade
The problem is not that the names look strange. The problem is that they depend entirely on default expressions. If the framework changes those expressions in a new major release, or someone changes APP_NAME one day, all three names change silently. Users' browsers still send the old cookie name, but the server looks for the new one. Everyone gets logged out at once. Meanwhile, Redis keys starting with the old prefixes become leftovers that nobody reads anymore. The entire cache is effectively discarded.
I did not compare the defaults in 12 and 13 line by line, so I cannot say whether the expressions actually changed. I therefore will not claim that the new major release changed them. The lesson holds either way: values users already have must not depend on defaults we did not write ourselves. I pinned them as a precaution.

First, check which apps are affected
Do not blindly pin every app yet. I first checked which apps actually derived all three names from default expressions. Apps that already had SESSION_COOKIE set in .env needed no action. Apps with a Thai APP_NAME were immediately recognizable by names starting with a hyphen. I wrote a short loop to print the actual values for every app in one table. It only reads; it changes nothing.
# พิมพ์ชื่อ cookie / cache prefix / redis prefix ที่แต่ละแอปใช้อยู่จริงตอนนี้
for app in /home/webuser/domains/example.com/app-a /home/webuser2/domains/example.org/app-b; do
printf '%s: ' "$app"
(cd "$app" && /usr/local/php84/bin/php artisan tinker --execute="
echo json_encode([config('session.cookie'), config('cache.prefix'), config('database.redis.options.prefix')]);")
echo
done
The output separates the apps into three groups: those already pinned, those using ordinary English names, and those with leading hyphens. The first two groups' names do not depend on the defaults in the risky way. The last group was the one where I needed to pin every app.
| Group | Example cookie name | At risk during a major upgrade? |
|---|---|---|
| SESSION_COOKIE already set in .env | Configured value | No risk; the name does not depend on the default |
| English APP_NAME | app-a-session | At risk if the default or app name changes |
| Thai APP_NAME | -session | Equally at risk, and harder to spot |
Read the actual values from the running app, then pin them in .env
The fix is straightforward: before running composer, read the values the app actually uses now and write those exact values into .env so they no longer depend on default expressions. I checked in two ways. First, I inspected the cookie the browser received from the live app. Second, I asked the app itself.
# ใช้ PHP ตัวเดียวกับที่ FPM ของแอปนี้ใช้ (ใส่พาธเต็ม)
cd /home/webuser/domains/example.com/app-a
/usr/local/php84/bin/php artisan tinker --execute="
echo config('session.cookie'), PHP_EOL;
echo config('cache.prefix'), PHP_EOL;
echo config('database.redis.options.prefix'), PHP_EOL;"
The results were the three lines I expected. I put them in .env, quoting every value because values starting with a hyphen are safer when quoted, even though the dotenv version in use could read them either way.
# .env (ค่าต้องตรงกับที่แอปใช้อยู่ตอนนี้เป๊ะ ๆ)
SESSION_COOKIE="-session"
CACHE_PREFIX="-cache-"
REDIS_PREFIX="-database-"
After the actual upgrade, users who were logged in stayed logged in, and the cache was not discarded. That was the result I wanted.

The point to remember is that pinning preserves continuity. If I actually want to change these names one day, I need to do it deliberately and tell users they will have to log in again once. It should not happen on its own because someone changes the app name.
Why not just change APP_NAME to English?
The likely next question is: if the app name causes this, can I just change it to English and be done? Yes, but not during a major upgrade, because changing the app name also changes the cookie name. The result is exactly the same: every user gets logged out and the cache is discarded. That gets mixed in with the risks of the version upgrade, making it hard to tell what caused any problems.
I split this into two jobs on separate days. On upgrade day, everything users can see stays the same. If I want nicer names later, I can schedule a day when requiring another login is acceptable, notify users in advance, and change the pinned SESSION_COOKIE value. I would not change it through APP_NAME, which is also used for other things, such as the name in emails.
Names depend on defaults
Saves one step during the upgrade, but if the defaults change or someone edits APP_NAME, every user gets logged out and the cache is discarded without anyone knowing why.
Names stay as written
Add three lines to .env per app. Users notice nothing during the major upgrade, and I can deliberately change the names later on a day I choose.
The batch upgrade procedure that worked for me
Once I had dealt with pinning, I added it to my batch upgrade script, which follows the sequence in this diagram for each app. The important part of the order is that pinning always comes before composer.
- Back up
vendor,composer.json,composer.lockand.envas a zstd tarball for each app - Run a dry run to see what will change, without touching the live app yet
- Pin
SESSION_COOKIE,CACHE_PREFIXandREDIS_PREFIXin.env - Upgrade to
^13only within the constraints composer allows - Run
composer audit; the result must be zero - Clear and rebuild the config, route and view caches
- Check that every runner for the app uses the correct PHP version
- Smoke test every page and test external callers
The backup in the first step provides a way back. If an app fails its smoke test or audit, the rollback is to extract that app's tarball over its files, clear the caches, and run the smoke test again to confirm the site is back to how it was before I touched it. I planned rollback per app, rather than for the whole batch, because apps that had passed should not be rolled back because another app broke.
# ย้อนกลับเฉพาะแอปที่ไม่ผ่าน
cd /home/webuser/domains/example.com/app-a
zstd -dc /root/pre-l13/app-a.tar.zst | tar -xf -
/usr/local/php84/bin/php artisan optimize:clear
Step 7 has its own trap: workers and cron jobs using a different PHP version from the web app. I covered that separately in the article on platform_check. Here, I will just say it needs checking before the job is finished. For step 5, I followed the same approach as the Laravel security checklist, using composer audit as the deciding check.
# สำรองก่อนทุกแอป (ตัวอย่างหนึ่งแอป)
cd /home/webuser/domains/example.com/app-a
tar -cf - vendor composer.json composer.lock .env | zstd -T2 -19 -o /root/pre-l13/app-a.tar.zst
composer audit # ก่อนอัป ต้องมี advisory ให้เห็น
composer update "laravel/*" --with-dependencies
composer audit # หลังอัป ต้องได้ No security vulnerability advisories found
The results were that advisories in the largest app dropped from several to zero, and all 16 apps passed with clean audits. I smoke tested 106 pages across 12 domains, and all returned 200. Laravel's logs had no new errors, the login pages and Livewire admin panels opened, and the queue workers were still running on PHP 8.4. Before touching the live apps, I had already run one app's test suite locally on 13, with 43 out of 43 passing.
In the first pass, one app was missing from my list because it was under a different hosting administrator from the batch I was checking. I switched to searching the entire machine for artisan files instead of working through accounts by owner.
# หาทุกแอป Laravel ทั้งเครื่อง ไม่สนว่าเจ้าของบัญชีเป็นใคร
find /home -maxdepth 6 -name artisan -not -path '*/vendor/*' 2>/dev/null | sort
External callers that POST without going through CSRF
One app in this batch continuously receives data from external devices that POST to an endpoint exempted from CSRF in bootstrap/app.php. I was concerned that the upgrade might stop those devices from sending data without my side noticing.
// bootstrap/app.php (endpoint ที่อุปกรณ์ภายนอกเรียก ตรวจสิทธิ์ด้วย API key ของตัวเอง)
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'devices/*',
]);
})
I used three checks. First, a POST without an API key had to return 401, which is the endpoint's own response. A 419 would mean CSRF was protecting the endpoint again. Second, the devices had to keep adding new rows to the table. Third, one manually triggered send had to return 200 with a body containing ok:true. This round passed all three checks, with no changes needed on the devices.
# ไม่มี key ต้องได้ 401 ไม่ใช่ 419
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://example.com/devices/push -d '{}'
One more thing: production code differed from my local source
Another issue I found along the way was that my local source for one app was far behind production because the tool previously used to develop it had been removed. I pulled the code from production into my workspace, ran composer install, and used a script to compare the files for drift. All 76 out of 76 files matched. The lesson is to always pull down what is actually running before changing anything, rather than assuming the local copy is the running version.
Another practice I am carrying forward is adding these three lines to .env from day one in every new app, with names I deliberately choose rather than names derived by converting the app name. The cost is three lines, in exchange for not having to worry about this during the next major upgrade or check every app retroactively as I did this time.
To sum up, before a major upgrade, record the cookie name and prefixes the app actually uses, pin them in .env, and only then touch composer. Whether to pin them is up to each site's needs if you are prepared to have every user log in again. I chose to keep everyone logged in. I will come back and make further edits later.
Want us to set this up?
Get help with server setup, configuration, migration, or troubleshooting. Save time for the work that matters.
Hit something like this? Stuck somewhere?
Come trade notes on the LinuxThai board



Comments 0
No comments yet — be the first