[{"data":1,"prerenderedAt":21874},["ShallowReactive",2],{"header-latest-en":3,"post-\u002Fblog\u002Flicense-builder":407,"all-posts-en":752},[4],{"id":5,"title":6,"body":7,"date":374,"description":375,"extension":376,"faq":377,"image":393,"lang":394,"meta":395,"navigation":396,"path":397,"published":396,"readTime":370,"seo":398,"stem":399,"tags":400,"updated":405,"__hash__":406},"blog\u002Fblog\u002Fen\u002Fdata-mocker.md","From Manual Fixtures to a Browser Data Mocker",{"type":8,"value":9,"toc":362},"minimark",[10,14,19,22,34,43,57,61,69,74,293,297,331,335,349,355,359],[11,12,13],"p",{},"In development, some tasks can't be fully automated — but they can be reduced to two clicks. Test data preparation is one of them.",[15,16,18],"h2",{"id":17},"from-manual-fixtures-to-fakerjs","From manual fixtures to Faker.js",[11,20,21],{},"For a long time, the only way to get test data was to write it by hand. Ten JSON objects with made-up names, addresses, and phones — a chore you can't delegate but can automate.",[11,23,24,25,29,30,33],{},"In 2012, ",[26,27,28],"strong",{},"Marak Squires"," released ",[26,31,32],{},"Faker.js"," — a library that generates realistic data: names, addresses, phone numbers, emails, text, and more. It quickly became the de facto standard, with ports appearing for PHP (FakerPHP), Python (Faker), Ruby (Faker), and others.",[11,35,36,37,42],{},"The idea is simple: instead of making up \"John Doe, ",[38,39,41],"a",{"href":40},"mailto:john@example.com","john@example.com",", +1-555-0100\" by hand, you tell the generator \"give me 50 users with name, email, and phone\" — and get a ready-made array.",[11,44,45,46,52,53,56],{},"Faker.js also has one of the more dramatic backstories in the npm ecosystem. In January 2022, frustrated that companies were using his free labor without paying for it, Squires deliberately pushed a corrupted release of faker.js (along with his other popular package, colors.js) that broke thousands of dependent projects — including tools built on AWS's CDK. npm reverted the malicious versions, but Squires' own package was effectively abandoned afterward. The community forked the last clean version as ",[26,47,48],{},[49,50,51],"code",{},"@faker-js\u002Ffaker",", which is the actively maintained project used today; the original ",[49,54,55],{},"faker"," package on npm is now deprecated.",[15,58,60],{"id":59},"data-mocker-a-schema-builder-in-the-browser","Data Mocker: a schema builder in the browser",[11,62,63,64,68],{},"Faker.js is powerful, but for a one-off task, installing a library and writing a script is overkill. ",[38,65,67],{"href":66},"\u002Ftools\u002Fdata-mocker","Data Mocker"," solves the same problem without setup:",[70,71,73],"h3",{"id":72},"_18-field-types","18 field types",[75,76,77,90],"table",{},[78,79,80],"thead",{},[81,82,83,87],"tr",{},[84,85,86],"th",{},"Type",[84,88,89],{},"What it generates",[91,92,93,104,114,124,134,144,154,164,174,192,202,220,230,240,250,260,270,283],"tbody",{},[81,94,95,101],{},[96,97,98],"td",{},[26,99,100],{},"Primary Key",[96,102,103],{},"Auto-incrementing IDs (1, 2, 3…) with configurable start value",[81,105,106,111],{},[96,107,108],{},[26,109,110],{},"Name",[96,112,113],{},"Random full names (first + last)",[81,115,116,121],{},[96,117,118],{},[26,119,120],{},"Email",[96,122,123],{},"Realistic email addresses from random name + domain",[81,125,126,131],{},[96,127,128],{},[26,129,130],{},"UUID",[96,132,133],{},"Unique identifiers (v4)",[81,135,136,141],{},[96,137,138],{},[26,139,140],{},"Phone",[96,142,143],{},"Phone numbers",[81,145,146,151],{},[96,147,148],{},[26,149,150],{},"Avatar URL",[96,152,153],{},"Profile picture placeholder URLs",[81,155,156,161],{},[96,157,158],{},[26,159,160],{},"ISO Date",[96,162,163],{},"Random dates within the past 2 years",[81,165,166,171],{},[96,167,168],{},[26,169,170],{},"Price",[96,172,173],{},"Random decimal prices (0–1000)",[81,175,176,181],{},[96,177,178],{},[26,179,180],{},"Number",[96,182,183,184,187,188,191],{},"Integers with configurable digit count (e.g. ",[49,185,186],{},"3"," for 100–999, ",[49,189,190],{},"1-4"," for 1–9999)",[81,193,194,199],{},[96,195,196],{},[26,197,198],{},"Boolean",[96,200,201],{},"Random true\u002Ffalse",[81,203,204,209],{},[96,205,206],{},[26,207,208],{},"Text \u002F Lorem",[96,210,211,212,215,216,219],{},"Lorem ipsum text — configurable paragraphs (",[49,213,214],{},"3p",") or character range (",[49,217,218],{},"100-500",")",[81,221,222,227],{},[96,223,224],{},[26,225,226],{},"City",[96,228,229],{},"Random city names from a pool of 40 cities",[81,231,232,237],{},[96,233,234],{},[26,235,236],{},"Country",[96,238,239],{},"Random country names from a pool of 40 countries",[81,241,242,247],{},[96,243,244],{},[26,245,246],{},"Company",[96,248,249],{},"Random company names (Acme Corp, Stark Industries, etc.)",[81,251,252,257],{},[96,253,254],{},[26,255,256],{},"URL",[96,258,259],{},"Random URLs with scheme, domain, and path",[81,261,262,267],{},[96,263,264],{},[26,265,266],{},"IPv4",[96,268,269],{},"Random IP addresses",[81,271,272,277],{},[96,273,274],{},[26,275,276],{},"Enum",[96,278,279,280,219],{},"Random pick from your own comma-separated values (e.g. ",[49,281,282],{},"active, inactive, pending",[81,284,285,290],{},[96,286,287],{},[26,288,289],{},"Custom",[96,291,292],{},"Literal text you type yourself",[70,294,296],{"id":295},"schema-management","Schema management",[298,299,300,307,313,319,325],"ul",{},[301,302,303,306],"li",{},[26,304,305],{},"Auto-save"," — your current schema is saved to localStorage on every change. Reload the page and it's still there.",[301,308,309,312],{},[26,310,311],{},"Save \u002F Load"," — save named schemas to reuse later. Click Save, type a name, done. Load picks from your saved schemas.",[301,314,315,318],{},[26,316,317],{},"Update"," — saving with an existing name overwrites it. The button text changes to \"Update\" automatically.",[301,320,321,324],{},[26,322,323],{},"Reset"," — one click restores the default schema.",[301,326,327,330],{},[26,328,329],{},"Export \u002F Import"," — download your schema as a JSON file, or import a schema from a file. Share schemas with your team.",[70,332,334],{"id":333},"output","Output",[298,336,337,340,343,346],{},[301,338,339],{},"Set row count from 1 to 200 using the stepper control",[301,341,342],{},"Switch between JSON and CSV output",[301,344,345],{},"Copy to clipboard or download as a file",[301,347,348],{},"Live preview updates as you edit the schema",[11,350,351,354],{},[26,352,353],{},"Quick start:"," open the tool → click \"Add field\" a few times → pick types (Name, Email, UUID) → set 10 rows → copy the JSON — ready test data for your component.",[15,356,358],{"id":357},"related-tools","Related tools",[357,360],{":slug":361},"[\"data-mocker\",\"json-toolbox\",\"sql-formatter\",\"regex-tester\"]",{"title":363,"searchDepth":364,"depth":364,"links":365},"",2,[366,367,373],{"id":17,"depth":364,"text":18},{"id":59,"depth":364,"text":60,"children":368},[369,371,372],{"id":72,"depth":370,"text":73},3,{"id":295,"depth":370,"text":296},{"id":333,"depth":370,"text":334},{"id":357,"depth":364,"text":358},"2026-08-30","Why realistic fixtures beat hand-written JSON, and how our local Data Mocker builds JSON or CSV from schemas with 18 field types.","md",[378,381,384,387,390],{"q":379,"a":380},"Why do I need a test data generator?","When developing a frontend or testing an API, you need realistic data — users, orders, products. Typing it by hand is tedious. A mock data generator creates a set of records from a schema in seconds.",{"q":382,"a":383},"What is Faker.js?","Faker.js is a library for generating fake data, created by Marak Squires in 2012. It can generate names, addresses, phones, emails, text, and more in dozens of languages. The original package is no longer maintained — in January 2022, Squires deliberately sabotaged his own faker.js in protest, and the community fork @faker-js\u002Ffaker is now the actively maintained, most widely used version in the JavaScript ecosystem.",{"q":385,"a":386},"Does Data Mocker send my data to a server?","No. Data Mocker runs entirely in the browser. All data is generated locally, nothing is sent to any server.",{"q":388,"a":389},"How is Data Mocker different from Faker.js?","Faker.js is a Node.js library you need to install and import. Data Mocker is a ready-to-use web page: open it, configure the schema, copy the JSON or CSV. No installation required, perfect for one-off data generation.",{"q":391,"a":392},"Can I save and reuse schemas?","Yes. Data Mocker auto-saves your current schema to localStorage. You can also save named schemas, load them later, reset to defaults, or export\u002Fimport schemas as JSON files.","\u002Fimages\u002Fblog\u002Fblog_data-mocker.webp","en",{},true,"\u002Fblog\u002Fen\u002Fdata-mocker",{"title":6,"description":375},"blog\u002Fen\u002Fdata-mocker",[401,32,402,403,67,404],"test data generation","mock data","test fixtures","schema builder",null,"QLLPi5vaiAcaslpKzeK9e2xT0ncMd009RwqjhFWtk9U",{"id":408,"title":409,"body":410,"date":721,"description":722,"extension":376,"faq":723,"image":739,"lang":394,"meta":740,"navigation":396,"path":741,"published":396,"readTime":742,"seo":743,"stem":744,"tags":745,"updated":405,"__hash__":751},"blog\u002Fblog\u002Fen\u002Flicense-builder.md","MIT, GPL, and Apache 2.0: How the Licenses Differ in Practice",{"type":8,"value":411,"toc":711},[412,415,418,422,432,441,454,463,467,480,492,495,499,508,517,520,527,531,546,549,553,629,632,636,639,656,659,663,682,685,703,706,708],[11,413,414],{},"An open-source license does not measure how generous or restrictive an author feels. It answers more concrete questions in advance: who may copy, modify, and distribute the code, which notices must remain, whether recipients must receive source, and how patent rights are handled. Framing MIT and GPL as a philosophical war obscures those mechanics. Both support commercial use and collaborative development, but they attach different conditions to what happens when software is passed on.",[11,416,417],{},"This article explains the general structure of four licenses and is not legal advice. A product involving material liability, outside investment, a large dependency graph, or a patent portfolio deserves review by qualified counsel before release.",[15,419,421],{"id":420},"the-gnu-gpl-unified-licenses-across-gnu-programs","The GNU GPL unified licenses across GNU programs",[11,423,424,425,431],{},"On September 27, 1983, Richard Stallman ",[38,426,430],{"href":427,"rel":428},"https:\u002F\u002Fwww.gnu.org\u002Fgnu\u002Finitial-announcement.html",[429],"nofollow","announced the GNU Project",", intending to build a free Unix-compatible system. Development began in January 1984, and the Free Software Foundation was founded in October 1985, initially to help fund GNU development. During this period, individual GNU programs used similar but not entirely identical license terms.",[11,433,434,435,440],{},"By 1989, enough practical experience had accumulated to replace those variants with a common license. ",[38,436,439],{"href":437,"rel":438},"https:\u002F\u002Fwww.gnu.org\u002Flicenses\u002Fold-licenses\u002Fgpl-1.0.html.en",[429],"GNU GPL version 1",", released in February 1989, used copyright not only to grant permissions to the first recipient but also to preserve those permissions downstream. This mechanism became known as copyleft: recipients may run, study, modify, and pass on the program, but conveying a covered modified work requires them to preserve the license and provide the next recipient access to source.",[11,442,443,444,447,448,453],{},"Copyleft does not make code “viral,” nor does it force every local modification onto a public website. GPLv3 obligations mainly arise on ",[49,445,446],{},"conveying",", when a copy is made available to another party. Internal use within one organization does not by itself require a public repository, as the ",[38,449,452],{"href":450,"rel":451},"https:\u002F\u002Fwww.gnu.org\u002Flicenses\u002Fgpl-faq.html#v3CoworkerConveying",[429],"GNU GPL FAQ"," explicitly explains. Ordinary GPLv3 also does not require server code to be released merely because users interact with it over a network; AGPLv3 contains a separate network condition for that scenario.",[11,455,456,457,462],{},"Later versions addressed new practical problems. GPLv2 followed in 1991, and ",[38,458,461],{"href":459,"rel":460},"https:\u002F\u002Fwww.gnu.org\u002Flicenses\u002Fquick-guide-gplv3.html",[429],"GPLv3 was published on June 29, 2007"," after a public drafting process. Version 3 treats patents and compatibility with some other licenses more explicitly. It also requires Installation Information in certain consumer-device distributions so that a recipient can install a modified build. This provision is commonly discussed under the label tivoization, but it is not a general promise that a vendor must support every user modification.",[15,464,466],{"id":465},"mit-continues-a-short-permissive-tradition","MIT continues a short permissive tradition",[11,468,469,470,473,474,479],{},"Permissive notices circulated through academic and Unix software before modern license catalogs standardized the identifier ",[49,471,472],{},"MIT",". The X Window System, created at MIT in the mid-1980s, provides a well-documented example. X.Org preserves the historical ",[38,475,478],{"href":476,"rel":477},"https:\u002F\u002Fwww.x.org\u002Farchive\u002FX11R6.8.0-orig\u002FPDF\u002FLICENSE.pdf",[429],"X\u002FMIT copyright texts",", which allowed software to be used, modified, distributed, and sold while requiring the copyright and permission notices to remain; a separate paragraph disclaimed warranties.",[11,481,482,483,485,486,491],{},"Today, the SPDX identifier ",[49,484,472],{}," points to the common short text published in the ",[38,487,490],{"href":488,"rel":489},"https:\u002F\u002Fopensource.org\u002Flicense\u002Fmit",[429],"Open Source Initiative catalog",". It grants permission to use, copy, modify, merge, publish, distribute, sublicense, and sell copies. Its condition is brief but real: the copyright notice and permission notice must accompany all copies or substantial portions of the software. A final paragraph disclaims warranties and limits liability to the extent applicable law allows.",[11,493,494],{},"Unlike copyleft, MIT does not require a larger product to use MIT or require source delivery with a binary. MIT-licensed code can therefore remain a separately attributed component inside a proprietary product as long as its notice condition is honored. That does not eliminate every legal risk. The text has no express patent grant, trademark rights do not automatically follow from a copyright license, and a contributor cannot grant rights they do not own in third-party code.",[15,496,498],{"id":497},"apache-20-states-more-of-the-arrangement-explicitly","Apache 2.0 states more of the arrangement explicitly",[11,500,501,502,507],{},"Apache License 2.0 is sometimes described as a longer or “corporate” MIT license, but that shorthand hides its origin. The Apache Software Foundation spent several years revising its own license to address contributions, documentation, notices, and patent risk more clearly. The ASF board ",[38,503,506],{"href":504,"rel":505},"https:\u002F\u002Fapache.org\u002Flicenses\u002Flicense-history.html",[429],"approved version 2.0 on January 21, 2004",".",[11,509,510,511,516],{},"The ",[38,512,515],{"href":513,"rel":514},"https:\u002F\u002Fwww.apache.org\u002Flicenses\u002FLICENSE-2.0",[429],"full Apache 2.0 text"," grants copyright and patent rights in separate sections. The copyright license permits reproduction, modification, and distribution. The patent grant covers only claims a contributor can license that would necessarily be infringed by the contribution alone or in combination with the work to which it was submitted. It is neither a guarantee against every patent claim nor a transfer of every patent the contributor owns.",[11,518,519],{},"Its patent-termination provision is narrower than many summaries suggest. If a recipient initiates patent litigation alleging that the work or an incorporated contribution infringes a patent, the Apache patent licenses granted to that recipient for the work terminate. The text does not say that any patent suit on any subject automatically cancels the copyright license.",[11,521,522,523,526],{},"An Apache distribution must include the license, mark modified files, and preserve relevant copyright, patent, trademark, and attribution notices. If the original work includes a ",[49,524,525],{},"NOTICE"," file, its applicable attributions must be reproduced in one of the permitted locations. Apache 2.0 nevertheless remains permissive: distributing a derivative does not by itself require the distributor's own modifications to be published as source.",[15,528,530],{"id":529},"unlicense-uses-a-different-legal-construction","Unlicense uses a different legal construction",[11,532,533,534,539,540,545],{},"Unlicense attempts to step away from downstream copyright conditions by dedicating a work to the public domain as fully as possible. Its ",[38,535,538],{"href":536,"rel":537},"https:\u002F\u002Funlicense.org\u002F",[429],"official text"," combines that intention with broad permission to use the software and a warranty disclaimer. The Unlicense is ",[38,541,544],{"href":542,"rel":543},"https:\u002F\u002Fopensource.org\u002Flicenses",[429],"approved by the Open Source Initiative",", which currently classifies it as a special-purpose license.",[11,547,548],{},"Public domain status and a license are not the same legal concept. Whether an author can fully waive copyright, and what that waiver accomplishes, varies by jurisdiction, which is why the Unlicense text describes its dedication in terms of jurisdictions that recognize copyright laws. It also lacks the express patent grant found in Apache 2.0. A project seeking an internationally predictable public-domain-style instrument should compare Unlicense with CC0-1.0 and its organization's policy rather than treating the two texts as interchangeable.",[15,550,552],{"id":551},"comparing-obligations-without-philosophical-labels","Comparing obligations without philosophical labels",[75,554,555,571],{},[78,556,557],{},[81,558,559,562,565,568],{},[84,560,561],{},"License",[84,563,564],{},"What happens when modifications are distributed",[84,566,567],{},"What must travel with the distribution",[84,569,570],{},"Express patent terms",[91,572,573,586,602,616],{},[81,574,575,577,580,583],{},[96,576,472],{},[96,578,579],{},"modifications may remain closed",[96,581,582],{},"copyright notice and permission notice",[96,584,585],{},"no separate patent grant",[81,587,588,591,593,599],{},[96,589,590],{},"Apache 2.0",[96,592,579],{},[96,594,595,596,598],{},"license, modification notices, applicable notices, and relevant ",[49,597,525],{}," content when present",[96,600,601],{},"contributor patent grant and limited termination condition",[81,603,604,607,610,613],{},[96,605,606],{},"GPLv3",[96,608,609],{},"a covered combined work is conveyed under GPLv3",[96,611,612],{},"license, notices, and Corresponding Source; sometimes Installation Information",[96,614,615],{},"patent grant and additional protective provisions",[81,617,618,621,624,627],{},[96,619,620],{},"Unlicense",[96,622,623],{},"author declares a copyright waiver and unrestricted use",[96,625,626],{},"the text is commonly retained to document provenance and the disclaimer",[96,628,585],{},[11,630,631],{},"This table describes typical cases; it does not decide whether a specific architecture forms one derivative or combined work. Dynamic loading, IPC, plugins, generated code, and components distributed by different parties require analysis of the actual relationship. A slogan such as “any link infects the project” cannot perform that analysis.",[15,633,635],{"id":634},"compatibility-always-has-a-direction","Compatibility always has a direction",[11,637,638],{},"Two licenses are compatible when code can be combined and conveyed while satisfying both sets of conditions. This does not mean that an original file changes its license, and it does not make compatibility symmetric. MIT code can join a GPLv3 program when its MIT notice remains and the covered combination is distributed under GPLv3. The reverse direction does not permit GPLv3 conditions to be stripped and the resulting code declared MIT.",[11,640,641,642,645,646,649,650,655],{},"The version matters too. Apache 2.0 is compatible with GPLv3 in the sense that Apache code can become part of a work conveyed under GPLv3. The FSF separately considers Apache 2.0 incompatible with GPLv2-only because of Apache's additional patent terms. Code marked ",[49,643,644],{},"GPL-2.0-or-later"," may have a path to GPLv3 that ",[49,647,648],{},"GPL-2.0-only"," code does not. The ",[38,651,654],{"href":652,"rel":653},"https:\u002F\u002Fwww.gnu.org\u002Flicenses\u002Fgpl-faq.html#WhatDoesCompatMean",[429],"GNU compatibility FAQ"," also points out that installing two independent programs on the same system does not combine them into one larger work.",[11,657,658],{},"License selection should therefore begin with the distribution model rather than popularity. A project needs to decide whether recipients of closed builds should receive source, whether the product is only offered as a network service, which licenses already govern dependencies, whether an express patent grant matters, and which notice procedures the release process can reliably maintain. When no standard license seems to fit, inventing a custom one usually creates more compatibility uncertainty than it resolves.",[15,660,662],{"id":661},"what-license-builder-actually-does","What License Builder actually does",[11,664,665,669,670,673,674,677,678,681],{},[38,666,668],{"href":667},"\u002Ftools\u002Flicense-builder","License & Readme Builder"," combines three independent functions. Its Compatibility Checker compares a source license with a target license across 11 common identifiers and returns ",[49,671,672],{},"Compatible",", ",[49,675,676],{},"Conditional",", or ",[49,679,680],{},"Incompatible"," with a short reason. A separate questionnaire maps preferences to one of four licenses for which the tool stores full text: MIT, Apache-2.0, GPLv3, and Unlicense. After a manual selection, the generator substitutes the copyright holder and year where the template provides those fields, then builds a README from a project title, description, installation command, usage example, and license badge.",[11,683,684],{},"The recommendation algorithm is intentionally coarse. A copyleft or network-source preference points to GPLv3, while the interface warns that ordinary GPLv3 does not contain the AGPL network clause. A request for an express patent grant points to Apache-2.0, and the remaining cases recommend MIT; Unlicense is available only through manual selection. All four choices permit commercial use, so this set cannot implement a noncommercial restriction, which would not meet the OSI definition of open source anyway. In the current implementation, the commercial-use and liability-protection checkboxes do not alter the recommendation formula; they only describe requested features, another reason not to treat the result as a complete licensing decision.",[11,686,687,688,690,691,694,695,698,699,702],{},"The Compatibility Checker is a reference aid rather than a legal conclusion. A ",[49,689,676],{}," result means that the answer depends on a version suffix, linking method, or another fact the short matrix cannot establish. Its combined display label ",[49,692,693],{},"Unlicense \u002F CC0-1.0"," is also a shortcut rather than a statement that the instruments are identical. For an actual release, verify the dependency's exact SPDX identifier, its ",[49,696,697],{},"-only"," or ",[49,700,701],{},"-or-later"," suffix, the full license text, and every required notice.",[11,704,705],{},"All input and generation stay in the browser. LICENSE and README can be copied together or downloaded separately, but the tool does not audit contribution ownership, project dependencies, CLAs, trademarks, or patent portfolios. Those remain part of the licensing process outside the generator.",[15,707,358],{"id":357},[357,709],{":slug":710},"[\"license-builder\",\"markdown-editor\",\"json-toolbox\",\"key-pair-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":712},[713,714,715,716,717,718,719,720],{"id":420,"depth":364,"text":421},{"id":465,"depth":364,"text":466},{"id":497,"depth":364,"text":498},{"id":529,"depth":364,"text":530},{"id":551,"depth":364,"text":552},{"id":634,"depth":364,"text":635},{"id":661,"depth":364,"text":662},{"id":357,"depth":364,"text":358},"2026-08-23","A practical comparison of MIT, GPLv3, Apache 2.0, and Unlicense, including copyleft, redistribution duties, patent grants, and compatibility.",[724,727,730,733,736],{"q":725,"a":726},"Who created the GNU GPL?","Richard Stallman developed early copyleft licenses for GNU programs, and the Free Software Foundation released the first general GNU General Public License in February 1989. It replaced several similar package-specific licenses with one set of terms.",{"q":728,"a":729},"What is the main difference between GPLv3 and MIT?","MIT permits its code to be included in closed products as long as the copyright and permission notices remain. When a modified or combined work covered by GPLv3 is conveyed, GPLv3 requires the covered work to remain under GPLv3 and its Corresponding Source to be available to recipients. The legal boundary of a combined work depends on the integration and applicable law.",{"q":731,"a":732},"Does the GPL require every private modification to be published?","No. GPLv3 ties its main source-delivery obligations to conveying copies to another party. Modifying and running a program internally does not by itself require an internet release. AGPLv3, rather than ordinary GPLv3, adds a source requirement for certain remote network interaction.",{"q":734,"a":735},"How does Apache 2.0 differ from MIT?","Apache 2.0 expressly covers contributions and patents, requires modified files to be marked, and may require relevant NOTICE attributions to travel with a distribution. MIT is shorter: its central condition is preserving the copyright and permission notices, and its text contains no comparable express patent grant.",{"q":737,"a":738},"What does license compatibility mean?","Two licenses are compatible when a combined distribution can satisfy both sets of terms. Compatibility is directional and version-specific: Apache-2.0 code can be incorporated into a GPLv3 work while retaining its conditions, but GPLv3 code cannot simply be relicensed as Apache-2.0. Merely placing separate programs on the same medium does not by itself combine them.","\u002Fimages\u002Fblog\u002Fblog_license-builder.webp",{},"\u002Fblog\u002Fen\u002Flicense-builder",7,{"title":409,"description":722},"blog\u002Fen\u002Flicense-builder",[746,747,748,749,590,750],"GPL vs MIT","Richard Stallman GPL history","which license to choose","what is copyleft","open source licenses","nVx8TB-xgn_HuPkwZrK9ww3LU70nwY9ttzM0ho804hI",[753,1016,1281,1515,1820,2049,2330,2564,2769,3170,3391,3548,3910,4397,4658,5004,5334,5563,5779,6050,6687,6915,7130,7355,7580,7852,8015,8248,8455,9354,10228,10603,10974,11238,11475,12796,13372,14005,14255,14921,15224,16581,17294,17977,19399,19699,19971,20240,21603],{"id":5,"title":6,"body":754,"date":374,"description":375,"extension":376,"faq":1007,"image":393,"lang":394,"meta":1013,"navigation":396,"path":397,"published":396,"readTime":370,"seo":1014,"stem":399,"tags":1015,"updated":405,"__hash__":406},{"type":8,"value":755,"toc":998},[756,758,760,762,768,772,780,782,786,788,954,956,978,980,990,994,996],[11,757,13],{},[15,759,18],{"id":17},[11,761,21],{},[11,763,24,764,29,766,33],{},[26,765,28],{},[26,767,32],{},[11,769,36,770,42],{},[38,771,41],{"href":40},[11,773,45,774,52,778,56],{},[26,775,776],{},[49,777,51],{},[49,779,55],{},[15,781,60],{"id":59},[11,783,63,784,68],{},[38,785,67],{"href":66},[70,787,73],{"id":72},[75,789,790,798],{},[78,791,792],{},[81,793,794,796],{},[84,795,86],{},[84,797,89],{},[91,799,800,808,816,824,832,840,848,856,864,876,884,896,904,912,920,928,936,946],{},[81,801,802,806],{},[96,803,804],{},[26,805,100],{},[96,807,103],{},[81,809,810,814],{},[96,811,812],{},[26,813,110],{},[96,815,113],{},[81,817,818,822],{},[96,819,820],{},[26,821,120],{},[96,823,123],{},[81,825,826,830],{},[96,827,828],{},[26,829,130],{},[96,831,133],{},[81,833,834,838],{},[96,835,836],{},[26,837,140],{},[96,839,143],{},[81,841,842,846],{},[96,843,844],{},[26,845,150],{},[96,847,153],{},[81,849,850,854],{},[96,851,852],{},[26,853,160],{},[96,855,163],{},[81,857,858,862],{},[96,859,860],{},[26,861,170],{},[96,863,173],{},[81,865,866,870],{},[96,867,868],{},[26,869,180],{},[96,871,183,872,187,874,191],{},[49,873,186],{},[49,875,190],{},[81,877,878,882],{},[96,879,880],{},[26,881,198],{},[96,883,201],{},[81,885,886,890],{},[96,887,888],{},[26,889,208],{},[96,891,211,892,215,894,219],{},[49,893,214],{},[49,895,218],{},[81,897,898,902],{},[96,899,900],{},[26,901,226],{},[96,903,229],{},[81,905,906,910],{},[96,907,908],{},[26,909,236],{},[96,911,239],{},[81,913,914,918],{},[96,915,916],{},[26,917,246],{},[96,919,249],{},[81,921,922,926],{},[96,923,924],{},[26,925,256],{},[96,927,259],{},[81,929,930,934],{},[96,931,932],{},[26,933,266],{},[96,935,269],{},[81,937,938,942],{},[96,939,940],{},[26,941,276],{},[96,943,279,944,219],{},[49,945,282],{},[81,947,948,952],{},[96,949,950],{},[26,951,289],{},[96,953,292],{},[70,955,296],{"id":295},[298,957,958,962,966,970,974],{},[301,959,960,306],{},[26,961,305],{},[301,963,964,312],{},[26,965,311],{},[301,967,968,318],{},[26,969,317],{},[301,971,972,324],{},[26,973,323],{},[301,975,976,330],{},[26,977,329],{},[70,979,334],{"id":333},[298,981,982,984,986,988],{},[301,983,339],{},[301,985,342],{},[301,987,345],{},[301,989,348],{},[11,991,992,354],{},[26,993,353],{},[15,995,358],{"id":357},[357,997],{":slug":361},{"title":363,"searchDepth":364,"depth":364,"links":999},[1000,1001,1006],{"id":17,"depth":364,"text":18},{"id":59,"depth":364,"text":60,"children":1002},[1003,1004,1005],{"id":72,"depth":370,"text":73},{"id":295,"depth":370,"text":296},{"id":333,"depth":370,"text":334},{"id":357,"depth":364,"text":358},[1008,1009,1010,1011,1012],{"q":379,"a":380},{"q":382,"a":383},{"q":385,"a":386},{"q":388,"a":389},{"q":391,"a":392},{},{"title":6,"description":375},[401,32,402,403,67,404],{"id":1017,"title":1018,"body":1019,"date":1249,"description":1250,"extension":376,"faq":1251,"image":1267,"lang":394,"meta":1268,"navigation":396,"path":1269,"published":396,"readTime":1270,"seo":1271,"stem":1272,"tags":1273,"updated":405,"__hash__":1280},"blog\u002Fblog\u002Fen\u002Ftext-diff.md","How Diff Chooses Changes and Why Git Has Several Algorithms",{"type":8,"value":1020,"toc":1240},[1021,1028,1031,1035,1046,1049,1052,1056,1065,1072,1075,1100,1106,1110,1130,1133,1139,1143,1146,1149,1164,1168,1176,1190,1194,1207,1210,1221,1235,1237],[11,1022,1023,1024,1027],{},"A ",[49,1025,1026],{},"diff"," result seems obvious until two versions contain repeated lines, reordered functions, or several equally plausible matches. The program cannot see the author's intent. It receives two sequences and must first decide which elements survived, then describe everything else as insertions and deletions.",[11,1029,1030],{},"That problem often has more than one minimum solution. Two edit scripts can contain the same number of operations, yet one keeps a new function together while another aligns braces and scatters the change across awkward hunks. Fast computation did not end the evolution of diff because edit count is only one concern; time, memory, stable alignment, and readability matter too.",[15,1032,1034],{"id":1033},"unix-diff-searched-for-the-lines-that-survived","Unix diff searched for the lines that survived",[11,1036,510,1037,1039,1040,1045],{},[49,1038,1026],{}," utility entered Fifth Edition Unix in 1974. In July 1976, James Hunt and Douglas McIlroy published the Bell Labs report ",[38,1041,1044],{"href":1042,"rel":1043},"https:\u002F\u002Fwww.cs.dartmouth.edu\u002F~doug\u002Fdiff.pdf",[429],"An Algorithm for Differential File Comparison",", which explained how the program was engineered. It aimed to produce a minimum list of line changes that transformed one file into another without consuming memory proportional to the product of their lengths on ordinary inputs.",[11,1047,1048],{},"Its central problem was the longest common subsequence, or LCS. Once the longest sequence of lines appearing in the same order in both files has been selected, those lines can remain while the rest are represented as insertions and deletions. LCS length is directly related to the size of a shortest edit script when a replacement counts as one deletion followed by one insertion.",[11,1050,1051],{},"The Hunt and McIlroy algorithm did not fill the straightforward dynamic-programming rectangle described in the old version of this article. It gained practical efficiency by selecting candidate matches, hashing lines, sorting them into equivalence classes, and merging candidates with binary search. The report observed behavior close to the sum of file lengths on normal document revisions, while acknowledging a product-of-lengths worst case. Repeated lines created especially many possible correspondences and remained difficult input.",[15,1053,1055],{"id":1054},"myers-connected-lcs-to-a-shortest-path","Myers connected LCS to a shortest path",[11,1057,1058,1059,1064],{},"In 1986, Eugene Myers published ",[38,1060,1063],{"href":1061,"rel":1062},"https:\u002F\u002Fneil.fraser.name\u002Fwriting\u002Fdiff\u002Fmyers.pdf",[429],"An O(ND) Difference Algorithm and Its Variations",", expressing LCS and shortest edit script as two views of a path through an edit graph. One axis contains the first sequence and the other the second. A horizontal edge is a deletion, a vertical edge is an insertion, and a cost-free diagonal edge is available when the two elements match.",[11,1066,1067],{},[1068,1069],"img",{"alt":1070,"src":1071},"Edit graph for finding a shortest sequence of insertions and deletions","\u002Fimages\u002Fblog\u002Finline_text-diff.webp",[11,1073,1074],{},"The algorithm considers paths with zero edits, then one, two, and so on until a path reaches the end of both sequences. On each diagonal it only needs to retain the furthest position reached because a path that arrived earlier cannot offer an advantage. After every edit, it follows the longest possible run of diagonal matches, commonly called a snake.",[11,1076,1077,1078,1081,1082,1085,1086,1089,1090,1092,1093,1095,1096,1099],{},"In the paper's notation, ",[49,1079,1080],{},"N"," is the sum of the two sequence lengths and ",[49,1083,1084],{},"D"," is the length of a minimum insertion-and-deletion script. The basic algorithm takes ",[49,1087,1088],{},"O(ND)"," time, which suits similar files because ",[49,1091,1084],{}," is usually far smaller than ",[49,1094,1080],{}," for a modest commit. Myers also described a linear-space refinement and derived ",[49,1097,1098],{},"O(N + D²)"," expected time under the paper's stochastic model.",[11,1101,1102,1103,1105],{},"A shortest path gives a minimum operation count but not always a unique placement for those operations. Repeated elements let different diagonal matches produce different hunk boundaries and different stories for the reader. An alignment can be optimal in ",[49,1104,1084],{}," and still be the less intelligible explanation of a change.",[15,1107,1109],{"id":1108},"what-default-diff-means-in-git","What default diff means in Git",[11,1111,510,1112,1117,1118,1121,1122,1125,1126,1129],{},[38,1113,1116],{"href":1114,"rel":1115},"https:\u002F\u002Fgit-scm.com\u002Fdocs\u002Fdiff-algorithm-option.html",[429],"current Git documentation"," describes ",[49,1119,1120],{},"default"," and ",[49,1123,1124],{},"myers"," as the basic greedy diff and still lists it as the default. That establishes a lineage from Myers; it does not mean Git contains nearly untouched 1986 code. Its modern implementation lives in the ",[49,1127,1128],{},"xdiff"," subsystem, works on prepared lines, and participates in a much wider output pipeline.",[11,1131,1132],{},"Several decisions can change the result even when the named algorithm stays the same. Git defines line boundaries, can invoke custom diff drivers, formats hunks, and enables an indent heuristic that shifts hunk boundaries towards more readable locations. Rename detection, word diff, and moved-line coloring happen at other stages rather than becoming properties of Myers itself.",[11,1134,510,1135,1138],{},[49,1136,1137],{},"minimal"," mode remains close to the default path but spends additional time to ensure the smallest possible diff. It is useful when edit count matters more than speed, although fewer operations do not necessarily explain a refactoring more clearly.",[15,1140,1142],{"id":1141},"patience-and-histogram-choose-different-anchors","Patience and histogram choose different anchors",[11,1144,1145],{},"Patience diff begins with common lines that occur exactly once on each side and uses them as unambiguous alignment points. It then compares the ranges between those anchors separately. In source code, this can prevent repeated blank lines, braces, or boilerplate calls from being chosen as the main matches, so a moved block often remains visually coherent.",[11,1147,1148],{},"The approach has limits. Not every region contains unique lines, and its selected anchors do not promise a globally minimum edit script. Patience is better understood as an alternative alignment strategy that is often easier to read, not as an algorithm guaranteed to win every refactoring.",[11,1150,1151,1152,1155,1156,1159,1160,1163],{},"Git describes ",[49,1153,1154],{},"histogram"," as an extension of patience that supports low-occurrence common elements in addition to strictly unique ones. That supplies more meaningful anchors when few lines are truly unique. Results still depend on the file, so Git exposes ",[49,1157,1158],{},"--diff-algorithm=myers|minimal|patience|histogram"," and the ",[49,1161,1162],{},"diff.algorithm"," configuration rather than declaring one permanent choice for all content.",[15,1165,1167],{"id":1166},"tokenization-comes-before-the-diff","Tokenization comes before the diff",[11,1169,1170,1171,673,1173,1175],{},"A diff algorithm does not compare abstract text. It compares a sequence of tokens. A line is the usual token for a patch, but the same method can align words, characters, sentences, or array elements. Granularity changes both ",[49,1172,1080],{},[49,1174,1084],{},", and the meaning of the output: line mode suits code, word mode explains an edited sentence, and character mode exposes a single changed mark in a short string.",[11,1177,1178,1179,1121,1182,1185,1186,1189],{},"The alignment then needs a presentation. Unified output with ",[49,1180,1181],{},"---",[49,1183,1184],{},"+++"," file headers and ",[49,1187,1188],{},"@@"," hunk markers is a format, not a different LCS algorithm. The same internal changes can appear as one patch stream, two columns, or highlighted spans inside a line.",[15,1191,1193],{"id":1192},"how-text-diff-works","How Text Diff works",[11,1195,510,1196,1200,1201,1206],{},[38,1197,1199],{"href":1198},"\u002Ftools\u002Ftext-diff","Text Diff"," loads jsdiff 7.0.0 and performs the comparison in the browser; the entered texts are not uploaded. The ",[38,1202,1205],{"href":1203,"rel":1204},"https:\u002F\u002Fgithub.com\u002Fkpdecker\u002Fjsdiff",[429],"jsdiff library"," is based on the Myers paper. It tokenizes each input at the requested granularity and finds a minimum set of single-token insertions and deletions.",[11,1208,1209],{},"Lines mode treats each complete line as a token and renders one colored list with line numbers. Words and Chars show inline changes at word or character granularity. Statistics count added, removed, and unchanged units in the current mode, so those numbers refer to lines only while Lines is selected. The inputs sit in two columns, but the diff result itself is not a side-by-side table.",[11,1211,1212,1213,1216,1217,1220],{},"Ignore whitespace also behaves differently by mode. Lines passes the option to ",[49,1214,1215],{},"diffLines","; Words and Chars first collapse runs of spaces and tabs. Copy unified diff always builds a fresh patch from the complete original texts with ",[49,1218,1219],{},"createTwoFilesPatch",", regardless of the on-screen mode. Whitespace ignoring affects that copied patch only when Lines is active.",[11,1222,1223,1224,673,1226,1228,1229,1231,1232,507],{},"The copied output includes familiar ",[49,1225,1181],{},[49,1227,1184],{},", and ",[49,1230,1188],{}," markers, but the tool does not reproduce Git's complete processing pipeline. It has no repository context, custom diff drivers, rename detection, indent heuristic, patience, or histogram mode. It is a local comparison of two texts that can produce ordinary unified output, not a browser implementation of every stage in ",[49,1233,1234],{},"git diff",[15,1236,358],{"id":357},[357,1238],{":slug":1239},"[\"text-diff\",\"regex-tester\",\"json-toolbox\",\"string-transformer\"]",{"title":363,"searchDepth":364,"depth":364,"links":1241},[1242,1243,1244,1245,1246,1247,1248],{"id":1033,"depth":364,"text":1034},{"id":1054,"depth":364,"text":1055},{"id":1108,"depth":364,"text":1109},{"id":1141,"depth":364,"text":1142},{"id":1166,"depth":364,"text":1167},{"id":1192,"depth":364,"text":1193},{"id":357,"depth":364,"text":358},"2026-08-29","From Unix diff and LCS to the Myers algorithm: how programs align lines and why Git offers minimal, patience, and histogram modes.",[1252,1255,1258,1261,1264],{"q":1253,"a":1254},"Who created Unix diff?","The diff utility appeared in Fifth Edition Unix in 1974. James Hunt and Douglas McIlroy documented its practical algorithm in a 1976 Bell Labs technical report. It found a longest common subsequence of lines, but selected candidate matches and used hashing, equivalence-class sorting, and binary search to make real files tractable.",{"q":1256,"a":1257},"What did the Myers algorithm change?","Eugene Myers represented the problem as a shortest-path search through an edit graph in 1986. If N is the combined sequence length and D is the minimum number of insertions and deletions, the basic algorithm takes O(ND) time. It is consequently efficient for similar files where D is much smaller than N.",{"q":1259,"a":1260},"Which algorithm does git diff use by default?","Current Git documentation calls default and myers the basic greedy diff and lists it as the default. Actual output also depends on configuration, tokenization, and the default indent heuristic that shifts hunk boundaries for readability. Git is based on the Myers family of ideas, not an untouched copy of the 1986 paper.",{"q":1262,"a":1263},"How do minimal, patience, and histogram differ in Git?","Minimal spends extra time to produce the smallest possible diff. Patience uses common lines that occur once on each side as stable alignment points, which can help with moved blocks and repetitive syntax. Histogram extends that idea to low-occurrence common lines. No mode produces the clearest result for every file.",{"q":1265,"a":1266},"Which modes does Text Diff provide?","The tool compares lines, words, or characters with jsdiff 7.0.0, which is based on the Myers algorithm. Its display is one colored change stream with statistics in the unit of the selected mode. A separate button creates unified output with ---, +++, and @@ headers from the original texts, independently of the on-screen granularity.","\u002Fimages\u002Fblog\u002Fblog_text-diff.webp",{},"\u002Fblog\u002Fen\u002Ftext-diff",5,{"title":1018,"description":1250},"blog\u002Fen\u002Ftext-diff",[1274,1275,1276,1277,1278,1279],"Myers algorithm","Unix diff history","Douglas McIlroy","how git diff works","file comparison","longest common subsequence","VUnzjoUQSujjX2IM2coCJBx_ppxveUh5_e-J5V078AE",{"id":1282,"title":1283,"body":1284,"date":1485,"description":1486,"extension":376,"faq":1487,"image":1503,"lang":394,"meta":1504,"navigation":396,"path":1505,"published":396,"readTime":1270,"seo":1506,"stem":1507,"tags":1508,"updated":405,"__hash__":1514},"blog\u002Fblog\u002Fen\u002Fcolor-toolbox.md","From CIE XYZ to OKLCH: Making Color More Perceptual",{"type":8,"value":1285,"toc":1476},[1286,1289,1293,1296,1305,1308,1312,1325,1328,1331,1334,1338,1347,1354,1362,1368,1372,1381,1384,1387,1390,1394,1421,1430,1436,1440,1447,1450,1461,1464,1471,1473],[11,1287,1288],{},"HEX, RGB, HSL, and OKLCH are not competing to become the one correct way to describe color. They were created for different jobs: RGB controls the contribution of display channels, HSL makes manual selection inside an RGB gamut easier, and perceptual models try to connect numerical changes with what an observer notices. Confusion begins when a convenient coordinate is mistaken for a physical measurement or a promise of identical visual results.",[15,1290,1292],{"id":1291},"cie-xyz-established-a-common-measurement-system","CIE XYZ established a common measurement system",[11,1294,1295],{},"By the early twentieth century, colors could already be reproduced by mixing light, but laboratories and manufacturers needed a common language that was not tied to one particular set of red, green, and blue phosphors. Color-matching experiments provided the foundation: an observer adjusted a mixture of three stimuli until it visually matched the color being studied.",[11,1297,1298,1299,1304],{},"In 1931, the International Commission on Illumination standardized those results as the CIE standard observer and the XYZ system. The current ",[38,1300,1303],{"href":1301,"rel":1302},"https:\u002F\u002Fwww.cie.co.at\u002Fpublications\u002Fcolorimetry-part-1-cie-standard-colorimetric-observers-0",[429],"CIE standard for colorimetric observers"," still includes the 1931 color-matching functions for a small field of view. X, Y, and Z describe a measured stimulus independently of a specific monitor, with the Y component connected to photometric luminance.",[11,1306,1307],{},"XYZ solved repeatable measurement, but it did not promise that equal geometric distances between points would appear as equal color differences. A small numerical movement could be obvious in one region and barely visible in another. Print, textile, paint, and photographic quality control needed a model in which the magnitude of an error corresponded more closely to observation.",[15,1309,1311],{"id":1310},"cielab-made-distance-more-closely-resemble-visible-difference","CIELAB made distance more closely resemble visible difference",[11,1313,1314,1315,1324],{},"By the 1970s, several nominally uniform color spaces and color-difference methods were in use. The CIE introduced CIELAB in 1976 to unify that diverse practice. As the current ",[38,1316,1319,1320,1323],{"href":1317,"rel":1318},"https:\u002F\u002Fcie.co.at\u002Fpublications\u002Fcolorimetry-part-4-cie-1976-lab-colour-space-0",[429],"CIE 1976 L",[1321,1322,38],"em",{},"b* standard"," explains, a simple Euclidean distance in the new coordinates was intended to approximate the magnitude of a perceived color difference.",[11,1326,1327],{},"CIELAB uses lightness L* and two opponent axes: a* runs from green to red, while b* runs from blue to yellow. Its nonlinear transformation of XYZ makes steps far more meaningful to human vision than distances between raw RGB components. This is why Lab became an important intermediate space in measurement and color management.",[11,1329,1330],{},"LCH does not replace Lab with a different theory of vision. It expresses the same points in cylindrical coordinates, replacing a* and b* with the distance from the neutral axis, called Chroma, and an angular Hue. This representation is convenient when the goal is to lighten a color, reduce its colorfulness, or rotate its hue without editing two opponent coordinates separately.",[11,1332,1333],{},"“Uniform” has always meant an approximation, however. CIELAB contains regions of unequal sensitivity, and changing colors near saturated blue can introduce an unwanted shift toward purple. More elaborate difference formulas such as CIE94 and CIEDE2000 improved specific comparisons, but they also added complexity and did not make interpolation in Lab ideal for graphics.",[15,1335,1337],{"id":1336},"why-hsl-is-useful-even-though-its-lightness-is-not-perceptual","Why HSL is useful even though its Lightness is not perceptual",[11,1339,1340,1341,1346],{},"Computer graphics developed alongside colorimetry but faced a different problem. Artists and video-system operators found it awkward to choose colors by directly changing three RGB channels, so the 1970s brought transformations that organized the RGB cube around Hue, Saturation, and either Value or Lightness. The 1978 paper ",[38,1342,1345],{"href":1343,"rel":1344},"https:\u002F\u002Falvyray.com\u002FPapers\u002FCG\u002Fcolor78.pdf",[429],"Color Gamut Transform Pairs"," described such models as alternative representations of an RGB monitor gamut that were useful in painting programs.",[11,1348,1349,1350,1353],{},"That history explains both the value and the limitation of HSL. Hue can be rotated around a circle, Saturation reduced toward gray, and Lightness adjusted to produce lighter or darker variants without manually mixing channels. HSL Lightness, however, is derived from the extremes of gamma-encoded RGB components and does not model the eye's varying sensitivity across the spectrum. Pure blue and pure yellow therefore both have ",[49,1351,1352],{},"L=50%",", even though yellow appears much lighter.",[11,1355,1356,1361],{},[38,1357,1360],{"href":1358,"rel":1359},"https:\u002F\u002Fwww.w3.org\u002FTR\u002Fcss-color-4\u002F",[429],"CSS Color 4"," explicitly describes HSL as more intuitive than RGB while also noting that its hues are unevenly spaced and that changing Hue alters visual lightness. HSL does not “lie”; it offers convenient control over sRGB geometry, but equal coordinate steps cannot be expected to produce an even-looking scale.",[11,1363,1364],{},[1068,1365],{"alt":1366,"src":1367},"Comparison of visual lightness non-uniformity in HSL with perceptual consistency in OKLCH","\u002Fimages\u002Fblog\u002Finline_color-toolbox.webp",[15,1369,1371],{"id":1370},"what-oklab-and-oklch-changed","What Oklab and OKLCH changed",[11,1373,1374,1375,1380],{},"In 2020, Björn Ottosson published ",[38,1376,1379],{"href":1377,"rel":1378},"https:\u002F\u002Fbottosson.github.io\u002Fposts\u002Foklab\u002F",[429],"Oklab"," as a compact perceptual space for image processing. He wanted lightness, chroma, and hue to interfere with each other less, color transitions to look more even, and the math to remain simple, stable, and compatible with the D65 white point used by sRGB and Display P3.",[11,1382,1383],{},"The conversion starts from linear RGB values, maps them into intermediate LMS-like components, applies nonlinear compression, and uses a second matrix to produce L, a, and b. Its parameters were optimized against data describing lightness, chroma, and hue while retaining a structure similar to the earlier IPT space. Ottosson also notes that some reference data for lightness and chroma came from CAM16 rather than directly from psychophysical experiments, so the results are not proof of a perfect match to human perception.",[11,1385,1386],{},"OKLCH relates to Oklab in the same way that LCH relates to CIELAB: it replaces a and b with Chroma and Hue while preserving Lightness. Designers get a familiar cylindrical model, but changing L tends to preserve the character of a hue more reliably, while gradients and palettes are less likely to fall into unexpectedly dark or gray regions. The improvement in hue constancy is particularly visible around blue, where CIELAB has known problems.",[11,1388,1389],{},"The model still has boundaries. It does not account for every viewing condition, replace a full color-appearance model for HDR, or guarantee accessible contrast. When Chroma is zero, Hue has no effect at all, while high Chroma values may fall outside the display gamut. Equal numerical distance also remains an approximation of visible difference and cannot represent variations in human vision.",[15,1391,1393],{"id":1392},"how-oklch-entered-css","How OKLCH entered CSS",[11,1395,1396,1397,677,1400,1403,1404,673,1407,673,1410,1228,1413,1416,1417,1420],{},"For many years, CSS color was largely limited to sRGB values written as HEX, ",[49,1398,1399],{},"rgb()",[49,1401,1402],{},"hsl()",". CSS Color Module Level 4 added ",[49,1405,1406],{},"lab()",[49,1408,1409],{},"lch()",[49,1411,1412],{},"oklab()",[49,1414,1415],{},"oklch()",", along with spaces such as Display P3 through ",[49,1418,1419],{},"color()",". The specification also lets a host syntax select an interpolation space, allowing a gradient or color mixture to travel through Oklab or OKLCH instead of legacy sRGB.",[11,1422,1423,1424,1429],{},"As of August 25, 2026, CSS Color 4 remains a Candidate Recommendation Draft, but the relevant functions have long been implemented. They became interoperable across the major browser engines in 2023, reaching Chromium 111, Firefox 113, and Safari 15.4, as documented by ",[38,1425,1428],{"href":1426,"rel":1427},"https:\u002F\u002Fweb.dev\u002Fblog\u002Fbaseline2023",[429],"Baseline 2023",". A fallback declaration is still sensible for old devices because a browser that does not recognize the new value will simply discard it.",[11,1431,1432,1433,1435],{},"The syntax does not expand the physical gamut of a screen. An ",[49,1434,1415],{}," color may sit inside sRGB, inside the wider Display P3 gamut, or outside both. The browser must map it to what the device can reproduce, so any scale with high Chroma should be checked for out-of-gamut colors.",[15,1437,1439],{"id":1438},"using-color-toolbox","Using Color Toolbox",[11,1441,1442,1446],{},[38,1443,1445],{"href":1444},"\u002Ftools\u002Fcolor-toolbox","Color Toolbox"," accepts HEX, RGB, and HSL, keeps those editable forms synchronized, and displays the selected color in OKLCH. The OKLCH row is a readout for copying rather than a reverse-conversion input, which makes the tool most useful for translating an existing sRGB color into modern CSS syntax.",[11,1448,1449],{},"Its harmony generator creates analogous, complementary, triadic, tetradic, monochrome, and shade palettes. One important detail is that these schemes are calculated in HSL, while OKLCH is an informational readout for the current color. A harmony is therefore a quick starting point, not a guarantee that every swatch has equal visual lightness.",[11,1451,1452,1453,1456,1457,1460],{},"The tool also includes more than 160 brand and design-system presets, a personal palette with editable names saved in ",[49,1454,1455],{},"localStorage",", and extraction of 2 to 12 dominant colors from an image. The image is downscaled and processed in a browser ",[49,1458,1459],{},"canvas"," with a median-cut-style quantizer; it is never uploaded.",[11,1462,1463],{},"The final panel calculates a relative-luminance contrast ratio and reports AA and AAA results separately for normal and large text. This illustrates an important boundary of perceptual color models: even a carefully constructed OKLCH scale still needs a separate accessibility check.",[11,1465,1466,1467,1470],{},"To see the distinction, enter ",[49,1468,1469],{},"#FF6B35",", compare its HSL Lightness with its OKLCH Lightness, choose the complementary harmony, and test a pair of resulting colors in the contrast checker. The tool then demonstrates three separate operations without conflating them: coordinate conversion, heuristic harmony generation, and readability testing.",[15,1472,358],{"id":357},[357,1474],{":slug":1475},"[\"color-toolbox\",\"qr-generator\",\"image-optimizer\",\"pixel-painter\",\"og-image-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":1477},[1478,1479,1480,1481,1482,1483,1484],{"id":1291,"depth":364,"text":1292},{"id":1310,"depth":364,"text":1311},{"id":1336,"depth":364,"text":1337},{"id":1370,"depth":364,"text":1371},{"id":1392,"depth":364,"text":1393},{"id":1438,"depth":364,"text":1439},{"id":357,"depth":364,"text":358},"2026-08-28","Why RGB and HSL solve a different problem from CIELAB and OKLCH, how perceptual color models evolved, and where their real limits remain.",[1488,1491,1494,1497,1500],{"q":1489,"a":1490},"How is OKLCH different from HSL?","Both models expose lightness, a measure of colorfulness, and a hue angle, but they derive those coordinates differently. HSL rearranges RGB geometrically and is convenient for manual color selection. OKLCH is the cylindrical form of Oklab and is designed to preserve perceived lightness, chroma, and hue more consistently as its numbers change.",{"q":1492,"a":1493},"Is OKLCH perfectly perceptually uniform?","No. Oklab is considerably more uniform than HSL and improves on CIELAB in many use cases, but it is an approximation rather than an exact simulation of every person's vision. Appearance also depends on surroundings, adaptation, patch size, display brightness, and individual differences between observers.",{"q":1495,"a":1496},"Do browsers support oklch() in CSS?","Yes. Oklab and OKLCH became interoperable across the major browser engines in 2023, with support in Chromium 111, Firefox 113, and Safari 15.4. For older browsers, an author can place a fallback HEX or RGB declaration first and follow it with the OKLCH value.",{"q":1498,"a":1499},"Does equal OKLCH Lightness guarantee accessible contrast?","No. The L coordinate helps produce more consistent scales, but text accessibility uses a separate contrast algorithm. Both colors, the text size and weight, and the chosen standard affect the verdict, so a palette still needs to be tested with a contrast checker.",{"q":1501,"a":1502},"Can a display show every valid OKLCH color?","No. A mathematically valid color may fall outside sRGB or even Display P3. The browser must map it into the device gamut, which can change its chroma or hue. The maximum displayable Chroma also depends on Lightness and Hue rather than one universal limit.","\u002Fimages\u002Fblog\u002Fblog_color-toolbox.webp",{},"\u002Fblog\u002Fen\u002Fcolor-toolbox",{"title":1283,"description":1486},"blog\u002Fen\u002Fcolor-toolbox",[1509,1510,1511,1512,1360,1513],"what is OKLCH","Oklab Björn Ottosson","HSL problems","color space","color for developers","hnfuEZSxmsBj0YQ2SPN3NptKjFFAVqflx3w1KVysRYQ",{"id":1516,"title":1517,"body":1518,"date":1790,"description":1791,"extension":376,"faq":1792,"image":1808,"lang":394,"meta":1809,"navigation":396,"path":1810,"published":396,"readTime":1270,"seo":1811,"stem":1812,"tags":1813,"updated":405,"__hash__":1819},"blog\u002Fblog\u002Fen\u002Fjson-toolbox.md","JSON Toolbox: Why JSON Fit Web APIs So Well",{"type":8,"value":1519,"toc":1782},[1520,1523,1527,1536,1539,1560,1563,1595,1602,1606,1615,1624,1627,1671,1678,1682,1695,1704,1732,1736,1743,1746,1749,1752,1756,1763,1766,1773,1775,1778],[11,1521,1522],{},"JSON is often described as the format that defeated XML by removing closing tags. That account fits neatly into one paragraph but does not explain the history very well. XML was designed primarily as general-purpose markup for documents and structured information exchange, while JSON grew out of the practical need to move application state between a browser and a server. JSON was indeed a better fit for many simple web APIs, but that does not make XML a mistake or mean the two formats model every problem in the same way.",[15,1524,1526],{"id":1525},"why-xml-was-created","Why XML was created",[11,1528,1529,1530,1535],{},"By the mid-1990s, HTML had shown the value of delivering documents over a network, but its fixed vocabulary was not enough for publishing, scientific, and industry-specific systems. SGML allowed communities to define their own markup languages, although it was too complex for broad use on the web. A W3C working group retained its central ideas in a smaller set of rules, which became the ",[38,1531,1534],{"href":1532,"rel":1533},"https:\u002F\u002Fwww.w3.org\u002FTR\u002F1998\u002FREC-xml-19980210",[429],"XML 1.0 Recommendation"," on February 10, 1998.",[11,1537,1538],{},"XML let authors define elements appropriate to their subject and preserve document structure rather than only a tree of application values. A paragraph could interleave ordinary text with emphasis, links, and formulas without turning the content into an artificial set of fields. Attributes described element properties, child order was retained, and comments and processing instructions were part of the syntax.",[11,1540,1541,1542,1547,1548,1553,1554,1559],{},"Simple XML did not require the whole family of related standards. Namespaces became necessary when a document combined vocabularies and applications needed to distinguish elements with the same local name; W3C standardized that mechanism ",[38,1543,1546],{"href":1544,"rel":1545},"https:\u002F\u002Fwww.w3.org\u002Fpress-releases\u002F1999\u002Fxml-namespaces\u002F",[429],"in January 1999",". ",[38,1549,1552],{"href":1550,"rel":1551},"https:\u002F\u002Fwww.w3.org\u002FTR\u002Fxslt.html",[429],"XSLT"," addressed the separate task of transforming an XML tree into another document, while ",[38,1555,1558],{"href":1556,"rel":1557},"https:\u002F\u002Fwww.w3.org\u002Fpress-releases\u002F2001\u002Fxml-schema\u002F",[429],"XML Schema",", released in 2001, described permitted structures, constraints, and types such as dates or integers. These technologies made the ecosystem larger, but they were created for vocabulary reuse, document validation, and complex automated exchange rather than for formatting one small object.",[11,1561,1562],{},"The cost of that generality is visible even in a small example:",[1564,1565,1569],"pre",{"className":1566,"code":1567,"language":1568,"meta":363,"style":363},"language-xml shiki shiki-themes github-dark","\u003Cperson>\n  \u003Cname>Alex\u003C\u002Fname>\n  \u003Cage>30\u003C\u002Fage>\n\u003C\u002Fperson>\n","xml",[49,1570,1571,1579,1584,1589],{"__ignoreMap":363},[1572,1573,1576],"span",{"class":1574,"line":1575},"line",1,[1572,1577,1578],{},"\u003Cperson>\n",[1572,1580,1581],{"class":1574,"line":364},[1572,1582,1583],{},"  \u003Cname>Alex\u003C\u002Fname>\n",[1572,1585,1586],{"class":1574,"line":370},[1572,1587,1588],{},"  \u003Cage>30\u003C\u002Fage>\n",[1572,1590,1592],{"class":1574,"line":1591},4,[1572,1593,1594],{},"\u003C\u002Fperson>\n",[11,1596,1597,1598,1601],{},"Without XML Schema, a parser sees the content of ",[49,1599,1600],{},"age"," as text. In return, the same model can add attributes, mixed content, and elements from other namespaces without changing formats. That is valuable for documents but often broader than an application object needs.",[15,1603,1605],{"id":1604},"how-json-emerged","How JSON emerged",[11,1607,1608,1609,1614],{},"In 2001, ",[38,1610,1613],{"href":1611,"rel":1612},"https:\u002F\u002Fwww.crockford.com\u002Fimage\u002Fhowjsworks.pdf",[429],"Douglas Crockford and Chip Morningstar"," were building a State Software platform for what would now be called single-page applications. The browser needed to exchange small pieces of state with a server without replacing the entire page, so the team wanted a representation that could be generated easily on the server and turned into values in JavaScript.",[11,1616,1617,1618,1623],{},"The suitable notation was already present in the language. Object and array literals expressed the same structures the application used. Crockford later stressed that he did not consider himself the first inventor of the technique because several developers had discovered it independently. His contribution was to apply the model at State Software in spring 2001, give it the name JavaScript Object Notation, acquire json.org in 2002, and ",[38,1619,1622],{"href":1620,"rel":1621},"https:\u002F\u002Fwww.json.org\u002Ffatfree.html",[429],"publish a description of the format",". Morningstar helped design the server system and the original data-exchange problem, while Crockford wrote the specification and later RFC.",[11,1625,1626],{},"The same simple record was compact and preserved the distinction between a string and a number:",[1564,1628,1632],{"className":1629,"code":1630,"language":1631,"meta":363,"style":363},"language-json shiki shiki-themes github-dark","{\n  \"name\": \"Alex\",\n  \"age\": 30\n}\n","json",[49,1633,1634,1640,1656,1666],{"__ignoreMap":363},[1572,1635,1636],{"class":1574,"line":1575},[1572,1637,1639],{"class":1638},"s95oV","{\n",[1572,1641,1642,1646,1649,1653],{"class":1574,"line":364},[1572,1643,1645],{"class":1644},"sDLfK","  \"name\"",[1572,1647,1648],{"class":1638},": ",[1572,1650,1652],{"class":1651},"sU2Wk","\"Alex\"",[1572,1654,1655],{"class":1638},",\n",[1572,1657,1658,1661,1663],{"class":1574,"line":370},[1572,1659,1660],{"class":1644},"  \"age\"",[1572,1662,1648],{"class":1638},[1572,1664,1665],{"class":1644},"30\n",[1572,1667,1668],{"class":1574,"line":1591},[1572,1669,1670],{"class":1638},"}\n",[11,1672,1673,1674,1677],{},"JSON used familiar JavaScript conventions but was not an arbitrary object literal. Member names require double quotes, and comments, functions, ",[49,1675,1676],{},"undefined",", single-quoted strings, and trailing commas are outside its grammar. Restricting the language was useful for data exchange because a receiver did not need to execute a general-purpose programming language.",[15,1679,1681],{"id":1680},"standardization-followed-real-world-use","Standardization followed real-world use",[11,1683,1684,1685,1690,1691,1694],{},"In July 2006, ",[38,1686,1689],{"href":1687,"rel":1688},"https:\u002F\u002Fwww.rfc-editor.org\u002Finfo\u002Frfc4627\u002F",[429],"RFC 4627"," registered the ",[49,1692,1693],{},"application\u002Fjson"," media type and formally described syntax that deployed systems already used. Douglas Crockford was the document's sole author. That first RFC allowed only an object or array at the top level; later specifications accepted any JSON value and clarified interoperability requirements.",[11,1696,1697,1698,1703],{},"The current main IETF document is ",[38,1699,1702],{"href":1700,"rel":1701},"https:\u002F\u002Fwww.rfc-editor.org\u002Finfo\u002Frfc8259\u002F",[429],"RFC 8259",", dated December 2017 and aligned with ECMA-404. It defines JSON as a language-independent text format and requires UTF-8 for exchange between systems. RFC 4627 remains an important standardization milestone, but it is no longer the final modern definition.",[11,1705,1706,1707,1710,1711,1648,1716,1718,1719,1729,1730,507],{},"Early browser applications sometimes parsed a response with ",[49,1708,1709],{},"eval()"," because valid JSON closely resembled a JavaScript expression. The convenience carried a ",[38,1712,1715],{"href":1713,"rel":1714},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc8259.html#section-12",[429],"serious risk",[49,1717,1709],{}," executes code with the page's privileges, so an untrusted response could become an XSS vulnerability. ECMAScript 5 added dedicated ",[38,1720,1723,1121,1726],{"href":1721,"rel":1722},"https:\u002F\u002F262.ecma-international.org\u002F5.1\u002F#sec-15.12",[429],[49,1724,1725],{},"JSON.parse()",[49,1727,1728],{},"JSON.stringify()"," methods in December 2009, removing any reason to execute incoming data. Modern code should use a JSON parser, never ",[49,1731,1709],{},[15,1733,1735],{"id":1734},"why-json-fit-web-apis","Why JSON fit web APIs",[11,1737,1738,1739,1742],{},"JSON's main advantage was not simplicity in the abstract but a close match between data models. Interface state, request parameters, and most API responses map naturally to objects, arrays, strings, numbers, booleans, and ",[49,1740,1741],{},"null",". Similar structures exist as Python dictionaries, PHP arrays, Go maps and structs, Java collections, and equivalents in many other languages, so the format quickly became independent of JavaScript in practice.",[11,1744,1745],{},"For messages of this shape, JSON usually needs less structural syntax than paired XML tags, and its small grammar makes parsers straightforward to implement. There is no universal percentage saving, however. Results depend on the structure, field names, use of XML attributes, and transport compression. Keys also repeat in large homogeneous JSON arrays, while binary formats can be much smaller than either textual option.",[11,1747,1748],{},"Minimalism introduces constraints of its own. JSON does not distinguish integers from fractional numbers, has no native dates or binary values, and does not support mixed document content, namespaces, or references between objects. JSON Schema can describe additional constraints, but it is separate from the base grammar. Implementations can also differ when they encounter duplicate member names or numbers beyond the exact range of a particular language.",[11,1750,1751],{},"XML remains stronger when content is both a document for people and structured input for machines, when several vocabularies must coexist in one file, or when an industry already relies on a rigorous exchange schema. JSON is usually more convenient for object-shaped web application messages and small APIs. The choice follows the shape and requirements of the data, not the outcome of an imaginary format war.",[15,1753,1755],{"id":1754},"what-json-toolbox-does","What JSON Toolbox does",[11,1757,1758,1762],{},[38,1759,1761],{"href":1760},"\u002Ftools\u002Fjson-toolbox","JSON Toolbox"," validates input with the browser's JSON parser as you type. When the engine reports a syntax-error position, the tool converts it to a line and column, marks the corresponding gutter line, and exposes the message in a tooltip.",[11,1764,1765],{},"After a successful parse, the output offers three views: syntax-highlighted Formatted with two spaces, four spaces, or tabs; a collapsible tree with value types; and a one-line Minified representation. The indentation setting is shared with the site's other formatting tools. Statistics report key and string counts, the combined number of characters in keys and string values, and byte sizes for the formatted and minified output. You can copy the Formatted or Minified representation, and all parsing happens locally without uploading the pasted data.",[11,1767,1768,1769,1772],{},"For a quick check, paste ",[49,1770,1771],{},"{\"users\":[{\"id\":1,\"name\":\"Alex\"},{\"id\":2,\"name\":\"Maria\"}]}",", move between Formatted, Tree, and Minified, then remove the comma between two members. The validator will mark the affected line and rebuild all three views as soon as the comma is restored.",[15,1774,358],{"id":357},[357,1776],{":slug":1777},"[\"json-toolbox\",\"yaml-json-converter\",\"api-sandbox\",\"sql-formatter\"]",[1779,1780,1781],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":363,"searchDepth":364,"depth":364,"links":1783},[1784,1785,1786,1787,1788,1789],{"id":1525,"depth":364,"text":1526},{"id":1604,"depth":364,"text":1605},{"id":1680,"depth":364,"text":1681},{"id":1734,"depth":364,"text":1735},{"id":1754,"depth":364,"text":1755},{"id":357,"depth":364,"text":358},"2026-08-27","How XML and JSON emerged, what Douglas Crockford and Chip Morningstar contributed, and why each format still has a useful domain.",[1793,1796,1799,1802,1805],{"q":1794,"a":1795},"Who created JSON?","JSON does not have a single inventor in the usual sense. Developers had transported data with JavaScript literal syntax before 2001; Douglas Crockford independently arrived at the idea at State Software, named the format, and published its description on json.org in 2002. Chip Morningstar worked with him on the system that needed this form of data exchange.",{"q":1797,"a":1798},"Why did JSON become popular for web APIs?","Its model of objects, arrays, and primitive values matched application data well, while its small syntax made messages easy to produce and parse. Adoption was also helped by JavaScript in the browser, standard JSON.parse and JSON.stringify methods, and later by built-in libraries across other languages.",{"q":1800,"a":1801},"Is JSON only for JavaScript?","No. The current specification defines JSON as a language-independent text format. Its syntax was derived from ECMAScript, but parsers and serializers exist in virtually every widely used language. Not every JavaScript literal is valid JSON either.",{"q":1803,"a":1804},"Is XML obsolete?","XML remains useful for marked-up documents, mixed text and elements, namespaces, attributes, strict industry vocabularies, schemas, and transformations. SVG, office formats, publishing systems, and many integration standards show that it is a different data model rather than an outdated copy of JSON.",{"q":1806,"a":1807},"Which types are missing from JSON?","JSON has strings, numbers, objects, arrays, true, false, and null. The base format has no separate date, binary, integer, or decimal types, so applications agree on string representations or use an additional schema. Comments and references between objects are also outside JSON syntax.","\u002Fimages\u002Fblog\u002Fblog_json-toolbox.webp",{},"\u002Fblog\u002Fen\u002Fjson-toolbox",{"title":1517,"description":1791},"blog\u002Fen\u002Fjson-toolbox",[1814,1815,1816,1817,1702,1818],"JSON and XML","JSON history","Douglas Crockford","Chip Morningstar","web APIs","8T549VR-MJELbSr2gxCZZjokqp360ijSIbHDNdMqs4c",{"id":1821,"title":1822,"body":1823,"date":2018,"description":2019,"extension":376,"faq":2020,"image":2036,"lang":394,"meta":2037,"navigation":396,"path":2038,"published":396,"readTime":1591,"seo":2039,"stem":2040,"tags":2041,"updated":405,"__hash__":2048},"blog\u002Fblog\u002Fen\u002Fepoch-counter.md","Why Unix Time Starts on January 1, 1970",{"type":8,"value":1824,"toc":2010},[1825,1832,1836,1849,1852,1855,1859,1874,1881,1892,1896,1907,1927,1933,1936,1940,1946,1961,1967,1971,1978,1988,1995,2005,2007],[11,1826,1827,1828,1831],{},"The date ",[49,1829,1830],{},"1970-01-01 00:00:00 UTC"," has become a shared coordinate origin for Unix-like systems, programming languages, and many data formats. It does not mark the beginning of computing, nor did it arrive fully formed with the first version of Unix. Both the unit and the epoch changed several times before the developers settled on the scheme we know today.",[15,1833,1835],{"id":1834},"the-first-unix-had-a-different-epoch","The first Unix had a different epoch",[11,1837,1838,1839,1844,1845,1848],{},"In the ",[38,1840,1843],{"href":1841,"rel":1842},"https:\u002F\u002Fwww.tuhs.org\u002FArchive\u002FDistributions\u002FResearch\u002FDennis_v1\u002F1stEdman.html",[429],"First Edition Unix manual",", dated November 1971, the ",[49,1846,1847],{},"time"," system call returned a 32-bit count of sixtieths of a second since January 1, 1971. That frequency matched the PDP-11 system clock, but it created an immediate limitation: the counter covered only about two and a half years.",[11,1850,1851],{},"To keep the counter usable, the early Unix developers moved its origin more than once and adjusted dates on existing files. In 1973 they adopted a more durable representation, storing whole seconds from the beginning of 1970. Moving from 60 ticks per second to one expanded the available span by roughly a factor of 60, while the new epoch remained close to the dates the system needed to represent.",[11,1853,1854],{},"The surviving historical material documents this sequence well, but it gives us no reason to turn January 1, 1970 into a symbolic date or attach an elaborate philosophy to it. It was a practical engineering decision that outlived its original hardware, became embedded in system interfaces, and grew expensive to replace as Unix spread.",[15,1856,1858],{"id":1857},"what-a-unix-timestamp-actually-counts","What a Unix timestamp actually counts",[11,1860,1861,1862,1867,1868,1873],{},"Modern POSIX ",[38,1863,1866],{"href":1864,"rel":1865},"https:\u002F\u002Fpubs.opengroup.org\u002Fonlinepubs\u002F9799919799\u002Fbasedefs\u002FV1_chap03.html",[429],"defines the Epoch"," as midnight on January 1, 1970 UTC and represents time as an approximate number of seconds after that point. “Approximate” matters here. The ",[38,1869,1872],{"href":1870,"rel":1871},"https:\u002F\u002Fpubs.opengroup.org\u002Fonlinepubs\u002F9799919799\u002Fbasedefs\u002FV1_chap04.html#tag_04_19",[429],"POSIX formula"," accounts for every calendar day as exactly 86,400 seconds, so a timestamp is not a continuous count of every physical second including leap-second insertions.",[11,1875,1876,1877,1880],{},"A timestamp does not contain a time zone either. The value ",[49,1878,1879],{},"0"," identifies one instant, but a local rendering may show the evening of December 31, 1969 west of Greenwich or the morning of January 1, 1970 to the east. Dates exchanged between systems should therefore carry their zone explicitly, with UTC normally used for an unambiguous machine value.",[11,1882,1883,1884,1887,1888,1891],{},"Strictly speaking, Unix timestamps are measured in seconds. Millisecond, microsecond, and nanosecond values are common extensions of the same convention, which means the unit should not be inferred from a generic field name. The number ",[49,1885,1886],{},"1700000000"," looks like seconds and ",[49,1889,1890],{},"1700000000000"," looks like milliseconds, but a reliable data format states the unit explicitly.",[15,1893,1895],{"id":1894},"where-the-year-2038-problem-comes-from","Where the Year 2038 problem comes from",[11,1897,1898,1899,1902,1903,1906],{},"A signed 32-bit integer ranges from −2,147,483,648 to 2,147,483,647. When such a field stores seconds after the Unix epoch, its last positive value represents ",[49,1900,1901],{},"2038-01-19 03:14:07 UTC","; the next instant, at ",[49,1904,1905],{},"03:14:08",", cannot be represented in that format.",[11,1908,1909,1910,1913,1914,1922,1923,1926],{},"This produces the familiar Y2038 illustration. With an ordinary two's-complement wrap, the next bit pattern is read as −2,147,483,648 and points to ",[49,1911,1912],{},"1901-12-13 20:45:52 UTC",". That does not mean every affected device will display 1901 at exactly that second. Depending on the language, library, and operation, a program may report an overflow, sort dates incorrectly, loop a timer, or fail in some other way. The Linux ",[38,1915,1918,1921],{"href":1916,"rel":1917},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman2\u002Ftime.2.html",[429],[49,1919,1920],{},"time()"," manual"," therefore gives the practical rule directly: applications intended to run after 2038 need an ABI with a ",[49,1924,1925],{},"time_t"," wider than 32 bits.",[11,1928,1929],{},[1068,1930],{"alt":1931,"src":1932},"Signed 32-bit Unix timestamp integer overflow at Year 2038 versus 64-bit time headroom","\u002Fimages\u002Fblog\u002Finline_epoch-counter.webp",[11,1934,1935],{},"The bug can surface well before the date itself. A mortgage calculation, certificate, booking, retention period, or scheduled job can already refer to an instant beyond the boundary. If any component in the path narrows the value to 32 bits, the failure happens when that future date is stored or processed, not only in January 2038.",[15,1937,1939],{"id":1938},"why-moving-to-64-bits-is-more-than-changing-one-type","Why moving to 64 bits is more than changing one type",[11,1941,1942,1943,1945],{},"In a new POSIX.1-2024 conforming environment, ",[49,1944,1925],{}," must be at least 64 bits wide. That range is more than sufficient for practical calendar work, but a 64-bit processor alone proves nothing: what matters is the ABI used by the program and every boundary the time value crosses.",[11,1947,1948,1949,1957,1958,1960],{},"A 32-bit Linux system, for example, can expose 64-bit time system calls while its C library provides them to applications through time64 interfaces. On traditional 32-bit glibc platforms, the ",[38,1950,1953,1956],{"href":1951,"rel":1952},"https:\u002F\u002Fsourceware.org\u002Fglibc\u002Fmanual\u002Flatest\u002Fhtml_node\u002FFeature-Test-Macros.html",[429],[49,1954,1955],{},"_TIME_BITS=64"," configuration"," changes ",[49,1959,1925],{}," and the related function interfaces. It does not rewrite an existing binary, nor does it automatically widen a 32-bit field in a database, file structure, network packet, or device firmware.",[11,1962,1963,1964,1966],{},"A useful Y2038 audit therefore follows the date through the complete data path instead of merely looking for 32-bit hardware. It checks operating-system and library interfaces, serialization, storage schemas, and boundary handling in business logic. Tests at ",[49,1965,1901],{},", the following second, and dates much farther in the future expose both obvious overflow and silent narrowing between formats.",[15,1968,1970],{"id":1969},"using-epoch-counter","Using Epoch Counter",[11,1972,1973,1977],{},[38,1974,1976],{"href":1975},"\u002Ftools\u002Fepoch-counter","Epoch Counter"," brings common Unix-time operations into one local tool. Its live counter shows the current instant in seconds, milliseconds, microseconds, and nanoseconds. The final two values are explicitly marked as having millisecond precision because appending zeroes cannot create precision that the browser clock did not provide.",[11,1979,1980,1981,698,1984,1987],{},"The converter infers a likely unit from the number of digits and accepts seconds, milliseconds, microseconds, or nanoseconds. It can display the result in UTC or local time, in human-readable and ISO 8601 forms. Conversion in the other direction accepts several common date formats; ",[49,1982,1983],{},"Z",[49,1985,1986],{},"GMT"," identifies UTC, while a string without a zone is parsed in the browser's local time.",[11,1989,1990,1991,1994],{},"Separate panels calculate the start and end of a day, month, or year, turn seconds into a duration, and provide common operations for twelve languages and databases, including JavaScript, Python, PHP, Go, Rust, PostgreSQL, and Shell. Everything runs in the browser, although very large inputs are still constrained by the range and numerical precision of JavaScript ",[49,1992,1993],{},"Date",", so the tool is intended for practical dates rather than arbitrary-precision arithmetic.",[11,1996,1997,1998,2001,2002,2004],{},"For a quick boundary check, enter ",[49,1999,2000],{},"2147483647"," as seconds and confirm that it becomes ",[49,2003,1901],{},". Add one, and Epoch Counter in a modern browser continues to produce the correct date. This makes the central point visible: the boundary belongs to an old 32-bit storage representation, not to the calendar or the Unix epoch itself.",[15,2006,358],{"id":357},[357,2008],{":slug":2009},"[\"epoch-counter\",\"cron-parser\",\"uuid-generator\",\"hash-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":2011},[2012,2013,2014,2015,2016,2017],{"id":1834,"depth":364,"text":1835},{"id":1857,"depth":364,"text":1858},{"id":1894,"depth":364,"text":1895},{"id":1938,"depth":364,"text":1939},{"id":1969,"depth":364,"text":1970},{"id":357,"depth":364,"text":358},"2026-08-26","How early Unix changed its time origin, why the Year 2038 problem exists, and which systems are actually still exposed to it.",[2021,2024,2027,2030,2033],{"q":2022,"a":2023},"Why does Unix time start on January 1, 1970?","This was not Unix's first epoch. The First Edition manual counted sixtieths of a second from January 1, 1971. After moving the origin several times, the developers switched in 1973 to whole seconds measured from the start of 1970. Historical sources document this technical evolution but do not support the popular story of one nearly accidental choice.",{"q":2025,"a":2026},"What happens on January 19, 2038?","At 03:14:07 UTC, a signed 32-bit seconds field reaches 2,147,483,647. The following second no longer fits. Interpreting the wrapped two's-complement value as −2,147,483,648 produces December 13, 1901, but a real program might instead return an error, compare dates incorrectly, or fail. The outcome depends on the system.",{"q":2028,"a":2029},"Are all 32-bit systems vulnerable to the Year 2038 problem?","No. The relevant question is the width of the time type and the entire data path, not just the processor. A 32-bit system can use 64-bit time_t and time64 interfaces, while an old binary, file format, or network protocol with a 32-bit time field remains limited even on new hardware.",{"q":2031,"a":2032},"Has the Year 2038 problem been solved?","For new POSIX environments and most 64-bit systems, using a time_t of at least 64 bits removes the practical limit. Older 32-bit ABIs need time64 support in the kernel and C library, the correct application build configuration, and sometimes migrations of databases, file formats, and protocols.",{"q":2034,"a":2035},"Does a Unix timestamp include leap seconds?","Ordinary POSIX time is not a continuous count of atomic seconds. The standard accounts for every calendar day as exactly 86,400 seconds and leaves the relationship between system time and actual UTC during adjustments to the implementation. Unix timestamps are useful for exchanging instants, but they do not replace specialized time scales for precise scientific measurement.","\u002Fimages\u002Fblog\u002Fblog_epoch-counter.webp",{},"\u002Fblog\u002Fen\u002Fepoch-counter",{"title":1822,"description":2019},"blog\u002Fen\u002Fepoch-counter",[2042,2043,2044,2045,2046,2047],"why UNIX time from 1970","Year 2038 problem","epoch time","Unix timestamp","32-bit time","what happens in 2038","TVM4GgkeECkpyBFItEpABcRt8Jnu7g8rSY9HBqYysFs",{"id":2050,"title":2051,"body":2052,"date":2299,"description":2300,"extension":376,"faq":2301,"image":2317,"lang":394,"meta":2318,"navigation":396,"path":2319,"published":396,"readTime":1270,"seo":2320,"stem":2321,"tags":2322,"updated":405,"__hash__":2329},"blog\u002Fblog\u002Fen\u002Fsql-formatter.md","How Codd's Relational Model Became SQL",{"type":8,"value":2053,"toc":2291},[2054,2057,2061,2064,2073,2082,2086,2089,2098,2120,2135,2138,2142,2151,2160,2175,2179,2194,2241,2248,2254,2258,2265,2268,2271,2280,2283,2285,2288],[11,2055,2056],{},"SQL did not begin as one finished idea waiting to be translated into code. Edgar Codd first separated the logical representation of data from its physical storage, Donald Chamberlin and Raymond Boyce then designed an approachable query language, and several research and commercial systems tested those decisions in practice. Standardization established a shared syntactic foundation only after implementations had already made choices of their own, which explains why modern SQL is recognizable across database systems yet still varies noticeably from one product to another.",[15,2058,2060],{"id":2059},"codd-separated-data-from-access-paths","Codd separated data from access paths",[11,2062,2063],{},"In the hierarchical and network database systems of the 1960s, an application often needed to know which links to follow and in what order to reach a record. This navigational approach tied a query to the storage design, so changing a structure or access path could require changes to the code that depended on it.",[11,2065,2066,2067,2072],{},"In his 1970 paper ",[38,2068,2071],{"href":2069,"rel":2070},"https:\u002F\u002Fresearch.ibm.com\u002Fpublications\u002Fa-relational-model-of-data-for-large-shared-data-banks",[429],"\"A Relational Model of Data for Large Shared Data Banks\"",", Codd made a different problem central. Users and most applications should work with a logical representation of data without depending on its internal organization. He described a model based on relations, meaning sets of tuples, together with operations that derive new relations from existing ones.",[11,2074,2075,2076,2081],{},"A table is a convenient way to display a relation, so rows and columns quickly became the familiar explanation of the model. Codd's proposal was not simply to replace one storage layout with tables, however. Its central contribution was data independence and the ability to state the desired result without manually traversing physical pointers. Codd received the ",[38,2077,2080],{"href":2078,"rel":2079},"https:\u002F\u002Fawards.acm.org\u002Faward_winners\u002F?172=",[429],"1981 ACM A.M. Turing Award"," for his contributions to the theory and practice of database management.",[15,2083,2085],{"id":2084},"the-mathematical-model-needed-a-language","The mathematical model needed a language",[11,2087,2088],{},"The relational model explained how to describe data and operations, but users still needed a practical way to write requests. IBM explored that problem through System R, a research system intended to test whether a relational interface could coexist with query optimization, transactions, recovery, and the other capabilities expected from a full database system.",[11,2090,2091,2092,2097],{},"Chamberlin and Boyce first worked on a language called SQUARE, then simplified its notation and moved it closer to English phrasing. Their 1974 paper ",[38,2093,2096],{"href":2094,"rel":2095},"https:\u002F\u002Fresearch.ibm.com\u002Fpublications\u002Fsequel-a-struciured-english-query-language",[429],"\"SEQUEL: A Structured English Query Language\""," presented a language for both professional programmers and people who used a database less frequently. A user named the required columns, sources, and conditions without prescribing a path through the stored records:",[1564,2099,2103],{"className":2100,"code":2101,"language":2102,"meta":363,"style":363},"language-sql shiki shiki-themes github-dark","SELECT name, age\nFROM users\nWHERE age > 18\n","sql",[49,2104,2105,2110,2115],{"__ignoreMap":363},[1572,2106,2107],{"class":1574,"line":1575},[1572,2108,2109],{},"SELECT name, age\n",[1572,2111,2112],{"class":1574,"line":364},[1572,2113,2114],{},"FROM users\n",[1572,2116,2117],{"class":1574,"line":370},[1572,2118,2119],{},"WHERE age > 18\n",[11,2121,2122,2123,2128,2129,2134],{},"SEQUEL stood for Structured English Query Language, but the name did not last. In ",[38,2124,2127],{"href":2125,"rel":2126},"https:\u002F\u002Farchive.computerhistory.org\u002Fresources\u002Faccess\u002Ftext\u002F2015\u002F06\u002F102702111-05-01-acc.pdf",[429],"Chamberlin's oral history",", the change is described in practical terms: the word was already somebody else's trademark, so he removed its vowels and produced SQL, which could still expand to Structured Query Language. At a ",[38,2130,2133],{"href":2131,"rel":2132},"https:\u002F\u002Fwww.mcjones.org\u002FSystem_R\u002FSQL_Reunion_95\u002Fsqlr95-System.html",[429],"1995 reunion of project members",", the trademark holder was identified as Britain's Hawker Siddeley. The trademark prompted a new name, but it was not a technical turning point in the design of the language.",[11,2136,2137],{},"Boyce was not a minor contributor to that work. He managed the language-oriented System R group and designed SEQUEL with Chamberlin, but died suddenly from a brain aneurysm in June 1974. Chamberlin later stressed that half of their joint work belonged to Boyce. His contribution also survives in the name Boyce-Codd Normal Form, which addresses anomalies and redundancy in relational schema design.",[15,2139,2141],{"id":2140},"system-r-was-a-major-branch-not-the-only-one","System R was a major branch, not the only one",[11,2143,2144,2145,2150],{},"System R is sometimes called the first relational DBMS, but that wording erases the parallel work of other teams. IBM's own 1976 architecture paper described ",[38,2146,2149],{"href":2147,"rel":2148},"https:\u002F\u002Fresearch.ibm.com\u002Fpublications\u002Fsystem-r-relational-approach-to-database-management",[429],"System R as a research vehicle rather than a planned product",". The project tested not only a query language but also access-path selection, cost-based optimization, locking, logging, and recovery, so its influence extended well beyond SQL syntax.",[11,2152,2153,2154,2159],{},"UC Berkeley was building Ingres at almost the same time. It also followed the relational model, but used QUEL, a language closely related to relational calculus. The original ",[38,2155,2158],{"href":2156,"rel":2157},"https:\u002F\u002Fpeople.eecs.berkeley.edu\u002F~wong\u002Fwong_pubs\u002Fwong40.pdf",[429],"description of Ingres"," reports that an initial QUEL implementation became operational in October 1974, and its authors credited SEQUEL with demonstrating the appeal of readable keywords. The two projects were not a simple sequence in which one team invented everything and another copied it. They turned a common theoretical foundation into working systems in different ways and learned from published ideas.",[11,2161,2162,2163,2168,2169,2174],{},"System R's practical results helped SQL move beyond a prototype. The project's ",[38,2164,2167],{"href":2165,"rel":2166},"https:\u002F\u002Fresearch.ibm.com\u002Fpublications\u002Fa-history-and-evaluation-of-system-r",[429],"1981 retrospective"," describes its goal as combining the usability of the relational model with the functions and performance needed for everyday production work. Its research influenced IBM SQL\u002FDS and DB2, while developers outside IBM could study the published language. Project members later assembled ",[38,2170,2173],{"href":2171,"rel":2172},"https:\u002F\u002Fwww.mcjones.org\u002FSystem_R\u002FSQL_Reunion_95\u002Fsqlr95.html",[429],"\"The 1995 SQL Reunion\"",", a detailed first-hand account that presents SQL as the product of researchers, product teams, and several companies rather than a single finished invention handed directly to the market.",[15,2176,2178],{"id":2177},"the-standard-defined-a-core-not-one-dialect","The standard defined a core, not one dialect",[11,2180,2181,2182,2187,2188,2193],{},"By the middle of the 1980s, SQL already existed in several products, so standards committees were documenting a language with real-world use and accumulated differences rather than starting from a blank page. The United States adopted ",[38,2183,2186],{"href":2184,"rel":2185},"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002FLegacy\u002FFIPS\u002Ffipspub127.pdf",[429],"ANSI X3.135-1986",", followed in June 1987 by the international ",[38,2189,2192],{"href":2190,"rel":2191},"https:\u002F\u002Fwww.iso.org\u002Fstandard\u002F16661.html",[429],"ISO 9075:1987",". The goal was portability for database definitions and applications, but a standard did not make every implementation discard its established syntax and proprietary features at once.",[11,2195,2196,2197,673,2200,1228,2203,2206,2207,2210,2211,2214,2215,2218,2219,2226,2227,2233,2234,507],{},"Later editions added more parts to SQL, while products implemented different versions, subsets, and extensions. PostgreSQL therefore has constructs such as ",[49,2198,2199],{},"ILIKE",[49,2201,2202],{},"RETURNING",[49,2204,2205],{},"ON CONFLICT","; MySQL provides ",[49,2208,2209],{},"AUTO_INCREMENT","; SQLite has its own ",[49,2212,2213],{},"PRAGMA"," statements and ",[49,2216,2217],{},"WITHOUT ROWID"," tables. The project documentation identifies these differences directly: PostgreSQL calls ",[38,2220,2223,2225],{"href":2221,"rel":2222},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ffunctions-matching.html",[429],[49,2224,2199],{}," a nonstandard extension",", MySQL documents ",[38,2228,2231],{"href":2229,"rel":2230},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fexample-auto-increment.html",[429],[49,2232,2209],{}," separately, and SQLite warns that ",[38,2235,2238,2240],{"href":2236,"rel":2237},"https:\u002F\u002Fwww.sqlite.org\u002Fpragma.html",[429],[49,2239,2213],{}," is specific to SQLite",[11,2242,2243,2244,2247],{},"SQL is not a literal notation for relational algebra either. The practical language includes schema definition, data modification, transaction control, ",[49,2245,2246],{},"NULL",", ordering, and mechanisms that were not part of Codd's original mathematical model in the same form. Its longevity comes less from theoretical purity than from a useful division of responsibility: developers describe the data they want, and the database system decides how to obtain it.",[11,2249,2250],{},[1068,2251],{"alt":2252,"src":2253},"Codd's relational model branched into System R (SEQUEL) and Ingres (QUEL), leading to SQL standardization and modern database dialects","\u002Fimages\u002Fblog\u002Finline_sql-formatter.webp",[15,2255,2257],{"id":2256},"what-the-sql-formatter-does","What the SQL Formatter does",[11,2259,510,2260,2264],{},[38,2261,2263],{"href":2262},"\u002Ftools\u002Fsql-formatter","SQL Formatter"," runs entirely in the browser and formats a query from its tokens without sending the text to a server. It recognizes keywords, strings, identifiers, line and block comments, and PostgreSQL dollar-quoted strings, then applies indentation and line breaks around the main clauses.",[11,2266,2267],{},"The interface provides four keyword vocabularies: Standard SQL, MySQL, PostgreSQL, and SQLite. Selecting a dialect improves recognition and highlighting for characteristic constructs, but it does not check the query grammar or guarantee that the chosen database will execute it. That distinction matters for unusual or complex syntax because the formatter uses tokenization rather than building a full syntax tree.",[11,2269,2270],{},"You can choose upper or lower case for keywords, indent with two spaces, four spaces, or a tab, and switch between formatted and compact output. The comparison shows the original and resulting UTF-8 byte sizes, the percentage change, and the output line count. To try it quickly, paste the query below, select Format, and then change the keyword case:",[1564,2272,2274],{"className":2100,"code":2273,"language":2102,"meta":363,"style":363},"select u.id,u.name,count(o.id) as orders from users u left join orders o on u.id=o.user_id where u.active=1 group by u.id,u.name having count(o.id)>5 order by orders desc\n",[49,2275,2276],{"__ignoreMap":363},[1572,2277,2278],{"class":1574,"line":1575},[1572,2279,2273],{},[11,2281,2282],{},"Formatting helps with reading, review, and documentation, but the parser of the target database must still validate the query before execution. That boundary reflects SQL's history: the language has a common framework, while the selected implementation gives some constructs their final meaning.",[15,2284,358],{"id":357},[357,2286],{":slug":2287},"[\"sql-formatter\",\"json-toolbox\",\"yaml-json-converter\",\"regex-tester\"]",[1779,2289,2290],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":2292},[2293,2294,2295,2296,2297,2298],{"id":2059,"depth":364,"text":2060},{"id":2084,"depth":364,"text":2085},{"id":2140,"depth":364,"text":2141},{"id":2177,"depth":364,"text":2178},{"id":2256,"depth":364,"text":2257},{"id":357,"depth":364,"text":358},"2026-08-25","How Codd's ideas passed through SEQUEL, System R, competition with QUEL, and standardization while SQL retained distinct dialects.",[2302,2305,2308,2311,2314],{"q":2303,"a":2304},"What exactly did Edgar Codd propose?","Codd proposed describing data as relations and working with it at a logical level, without requiring users to know the physical access paths. A table is a convenient representation of a relation, but the relational model is broader than the familiar tabular interface.",{"q":2306,"a":2307},"Who created SQL?","Donald Chamberlin and Raymond Boyce developed SEQUEL at IBM for the System R project and published its description in 1974. When they learned that SEQUEL was already a trademark, Chamberlin removed the vowels and renamed the language SQL.",{"q":2309,"a":2310},"Was System R the first relational DBMS?","System R was one of the most important early prototypes, but it was neither the only implementation nor an uncontested first. UC Berkeley was developing Ingres with the QUEL language at roughly the same time, while other groups also explored relational systems.",{"q":2312,"a":2313},"Why does SQL still have dialects after standardization?","Commercial products began implementing SQL before the first standard appeared, then evolved at different speeds and added their own capabilities. The standards established a common core rather than identical implementations, so PostgreSQL, MySQL, SQLite, and other systems still differ in syntax and features.",{"q":2315,"a":2316},"Does the SQL Formatter validate a query?","No. The tool recognizes tokens, keywords, strings, and comments, then applies line breaks and indentation. Selecting a dialect changes the highlighting vocabulary, but it does not turn the formatter into a parser, validator, or SQL execution environment.","\u002Fimages\u002Fblog\u002Fblog_sql-formatter.webp",{},"\u002Fblog\u002Fen\u002Fsql-formatter",{"title":2051,"description":2300},"blog\u002Fen\u002Fsql-formatter",[2323,2324,2325,2326,2327,2328],"SQL history","Codd relational model","Donald Chamberlin SQL","SEQUEL IBM","how SQL was created","database history","jZgM5c9BzjLbVdK8G_Ncw-AhAQ67quvuTlyQgsuNMKE",{"id":2331,"title":2332,"body":2333,"date":2535,"description":2536,"extension":376,"faq":2537,"image":2553,"lang":394,"meta":2554,"navigation":396,"path":2555,"published":396,"readTime":1591,"seo":2556,"stem":2557,"tags":2558,"updated":405,"__hash__":2563},"blog\u002Fblog\u002Fen\u002Fapi-sandbox.md","How curl Grew From httpget Into Network Infrastructure",{"type":8,"value":2334,"toc":2527},[2335,2341,2345,2360,2368,2372,2383,2390,2396,2400,2403,2406,2409,2413,2416,2431,2434,2438,2468,2494,2511,2522,2524],[11,2336,1023,2337,2340],{},[49,2338,2339],{},"curl"," command appears in countless API guides because it is easy to paste into a terminal, use for a quick request, and later translate into application code. The familiar command has a longer story behind it. The project began as a small program for retrieving data from the internet, changed names as its scope expanded, and eventually gained the libcurl library, which placed the same transfer engine inside many other programs.",[15,2342,2344],{"id":2343},"an-irc-bot-and-someone-elses-source-code","An IRC bot and someone else's source code",[11,2346,2347,2348,2351,2352,2355,2356,2359],{},"In late 1996, Swedish developer ",[26,2349,2350],{},"Daniel Stenberg"," was writing an IRC bot for an Amiga channel on EFnet. He wanted to add currency conversion, and the exchange rates were already available on the web, so the missing piece was a way to retrieve the page automatically. Rather than build an HTTP client from scratch, Stenberg used the open-source ",[26,2353,2354],{},"HttpGet",", whose version 0.1 had been released by Brazilian developer ",[26,2357,2358],{},"Rafael Sagula"," on November 11, 1996.",[11,2361,2362,2363,507],{},"Stenberg made the changes his bot needed, and version 0.2 with his additions followed on December 17. He soon became the project's maintainer, while the original practical problem continued to shape the software: it needed to retrieve data from several kinds of internet source, not demonstrate a single protocol. This sequence is recorded in curl's ",[38,2364,2367],{"href":2365,"rel":2366},"https:\u002F\u002Fcurl.se\u002Fdocs\u002Fhistory.html",[429],"official project history",[15,2369,2371],{"id":2370},"why-httpget-became-urlget-and-then-curl","Why httpget became urlget and then curl",[11,2373,2374,2375,2378,2379,2382],{},"The name ",[49,2376,2377],{},"httpget"," described the early program accurately while it mainly handled HTTP downloads. Support for Gopher and FTP made that description too narrow, so the project became ",[49,2380,2381],{},"urlget"," in August 1997: it now addressed resources by URL instead of only fetching HTTP pages. FTP uploads and HTTP POST support soon made the word “get” misleading as well, because the tool was no longer limited to receiving data.",[11,2384,2385,2386,2389],{},"On March 20, 1998, Stenberg released ",[26,2387,2388],{},"curl 4",", preserving the version sequence from the earlier names. The new name was associated with “client for URLs,” while the word curl also visibly contained URL. The reasoning behind the rename mattered more than the label itself: every added protocol, authentication method, and transfer mode was turning a small downloader into a general-purpose client useful well beyond the original IRC bot.",[11,2391,2392,2393,2395],{},"The project accumulated practical capabilities quickly during the late 1990s. SSL, cookies, Telnet, builds for several operating systems, and Linux packages all followed. Users now had a capable command, but it was still a separate program. Another product either had to launch a ",[49,2394,2339],{}," process or solve the same networking problems again inside its own code.",[15,2397,2399],{"id":2398},"libcurl-turned-a-program-into-a-building-block","libcurl turned a program into a building block",[11,2401,2402],{},"In the spring of 2000, curl underwent a substantial internal redesign to provide a library interface, and version 7.1 delivered the first non-beta libcurl API that August. A third-party PHP binding appeared in the same month. This was more consequential than adding one more protocol because developers could now embed the transfer engine in their own software and control it through library calls.",[11,2404,2405],{},"The command-line tool and library solve related but distinct problems. The CLI suits a person, shell script, or build system: a command describes a transfer, runs, and returns a result. libcurl lives inside a long-running application, where the program controls connections, data callbacks, concurrent operations, and error handling. It does so without separately implementing URL handling, proxies, authentication, TLS, cookies, and the details of every supported protocol.",[11,2407,2408],{},"That library is what carried curl far beyond the terminal. An application user may never see the name even though libcurl performs its transfers. The command-line version also spread as a standard system utility, but an exact installation count is impossible because curl ships with operating systems, travels through many mirrors, and arrives as a component of other software. Nor can the project's longevity be reduced to one author. Stenberg remains its lead developer, while an international community has long shared development, testing, and issue triage.",[15,2410,2412],{"id":2411},"websocket-addresses-a-different-networking-problem","WebSocket addresses a different networking problem",[11,2414,2415],{},"It is useful to picture an HTTP request as a completed operation: the client supplies a method, address, headers, and perhaps a body, then the server returns a response. Modern HTTP can reuse connections and stream data, so the distinction from WebSocket is not simply that “HTTP closes while WebSocket stays open.” The interaction model is the more important difference.",[11,2417,2418,2423,2424,1121,2427,2430],{},[38,2419,2422],{"href":2420,"rel":2421},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc6455",[429],"RFC 6455",", published by the IETF in December 2011 and authored by Ian Fette and Alexey Melnikov, standardized a protocol for two-way messaging between a browser client and server. A connection starts with an HTTP\u002F1.1 request containing ",[49,2425,2426],{},"Upgrade: websocket",[49,2428,2429],{},"Connection: Upgrade",". Once the server accepts that transition, communication continues as WebSocket frames over the same TCP connection, and either peer can send messages independently.",[11,2432,2433],{},"This model suits chats, collaborative editors, live dashboards, and other interfaces where new data does not arrive only in response to another user request. WebSocket does not replace an HTTP API. Applications commonly use ordinary requests to load and modify resources, while reserving a persistent connection for events that should arrive without repeatedly polling the server.",[15,2435,2437],{"id":2436},"what-api-sandbox-actually-does","What API Sandbox actually does",[11,2439,2440,2444,2445,698,2448,2451,2452,2455,2456,2459,2460,2463,2464,2467],{},[38,2441,2443],{"href":2442},"\u002Ftools\u002Fapi-sandbox","API & Web Sandbox"," places these two everyday jobs in adjacent tabs. cURL Converter does not execute the request or attempt to reproduce all of curl. It parses the URL, a method supplied with ",[49,2446,2447],{},"-X",[49,2449,2450],{},"--request",", headers from ",[49,2453,2454],{},"-H",", several ",[49,2457,2458],{},"--data"," forms, Basic Auth through ",[49,2461,2462],{},"-u",", and an explicit ",[49,2465,2466],{},"--url",". It then translates that request structure into one of twenty outputs, including Fetch API, Axios, native Node.js, Python, PHP, Go, C#, Java, Swift, Rust, HTTPie, Wget, PowerShell, and a Postman collection.",[11,2469,2470,2471,673,2474,673,2477,673,2480,673,2483,1228,2486,2489,2490,2493],{},"An unknown option produces a visible error, although several command-line flags such as ",[49,2472,2473],{},"--location",[49,2475,2476],{},"--insecure",[49,2478,2479],{},"--silent",[49,2481,2482],{},"--include",[49,2484,2485],{},"--verbose",[49,2487,2488],{},"--compressed"," are skipped because they describe curl execution or output rather than the core request structure. The result should therefore be treated as a code starting point. The converter does not carry over file uploads and multipart forms, cookies, client certificates, proxies, advanced authentication schemes, or every shell behavior. Generated browser ",[49,2491,2492],{},"fetch"," code is also subject to CORS and other browser restrictions even when the original command works from a terminal.",[11,2495,2496,2497,2500,2501,698,2504,2507,2508,2510],{},"WebSocket Tester, by contrast, opens a real connection through the browser's ",[49,2498,2499],{},"WebSocket"," API. It accepts a ",[49,2502,2503],{},"ws:\u002F\u002F",[49,2505,2506],{},"wss:\u002F\u002F"," address, sends text messages, shows the time and direction of each event, retains the latest 200 log entries, and can format JSON. Binary messages are identified without displaying their contents, while custom headers, subprotocol selection, and frame-level controls are not exposed. A page loaded over HTTPS should also use ",[49,2509,2506],{},", since the browser may block an insecure WebSocket as mixed content.",[11,2512,2513,2514,673,2516,1228,2518,2521],{},"For a quick check, paste a simple command using ",[49,2515,2447],{},[49,2517,2454],{},[49,2519,2520],{},"-d"," into the converter, compare the generated variants, and notice which parts of the request survive in each language. In the second tab, connect to your own test WS\u002FWSS server. The log will show the connection opening, sent messages, replies, and the close code without sending those details through an API Sandbox intermediary.",[15,2523,358],{"id":357},[357,2525],{":slug":2526},"[\"api-sandbox\",\"json-toolbox\",\"jwt-inspector\",\"key-pair-generator\",\"telegram-webhook-tester\",\"telegram-stars-invoice\",\"webhook-verifier\"]",{"title":363,"searchDepth":364,"depth":364,"links":2528},[2529,2530,2531,2532,2533,2534],{"id":2343,"depth":364,"text":2344},{"id":2370,"depth":364,"text":2371},{"id":2398,"depth":364,"text":2399},{"id":2411,"depth":364,"text":2412},{"id":2436,"depth":364,"text":2437},{"id":357,"depth":364,"text":358},"2026-08-24","The history of httpget, urlget, curl and libcurl, how WebSocket works, and an accurate guide to the browser-based API Sandbox.",[2538,2541,2544,2547,2550],{"q":2539,"a":2540},"Who created curl and when?","Rafael Sagula released HttpGet 0.1 on November 11, 1996. Daniel Stenberg adapted it for an IRC bot, soon became its maintainer, and continued developing the project. After a period under the name urlget, the first curl release kept the existing version sequence and arrived as curl 4 on March 20, 1998.",{"q":2542,"a":2543},"What is the difference between curl and libcurl?","curl is a ready-made command-line program controlled by a user or script. libcurl is the library from the same project: an application links to it and uses the transfer features through an API without launching a separate process or implementing the networking itself.",{"q":2545,"a":2546},"Why did curl become so widespread?","The project combines portability, support for multiple protocols, and long-term compatibility. The command-line tool is useful in terminals and automation, while libcurl can be embedded in other software, so many people rely on the technology without ever typing a curl command.",{"q":2548,"a":2549},"What is WebSocket?","WebSocket was standardized in RFC 6455 in December 2011. A connection begins with an HTTP-compatible opening handshake, after which the peers exchange frames over TCP and can independently send messages in either direction.",{"q":2551,"a":2552},"What can API Sandbox do?","The converter parses the basic structure of a curl command: its URL, method, headers, body, and Basic Auth, then offers twenty code and export variants. WebSocket Tester opens a WS\u002FWSS connection from the browser, sends text messages, and records an event log. It is a learning-oriented converter, not a complete implementation of every curl option or a specialized WebSocket client.","\u002Fimages\u002Fblog\u002Fblog_api-sandbox.webp",{},"\u002Fblog\u002Fen\u002Fapi-sandbox",{"title":2332,"description":2536},"blog\u002Fen\u002Fapi-sandbox",[2559,2350,2560,2561,2562],"curl history","libcurl","WebSocket RFC 6455","API Sandbox","1Ykh85ZdnDsLnLoxnfpdFn-tLhhfMRPL7YF6a8AOqkU",{"id":408,"title":409,"body":2565,"date":721,"description":722,"extension":376,"faq":2760,"image":739,"lang":394,"meta":2766,"navigation":396,"path":741,"published":396,"readTime":742,"seo":2767,"stem":744,"tags":2768,"updated":405,"__hash__":751},{"type":8,"value":2566,"toc":2750},[2567,2569,2571,2573,2578,2583,2590,2595,2597,2604,2611,2613,2615,2620,2625,2627,2631,2633,2641,2643,2645,2703,2705,2707,2709,2718,2720,2722,2732,2734,2744,2746,2748],[11,2568,414],{},[11,2570,417],{},[15,2572,421],{"id":420},[11,2574,424,2575,431],{},[38,2576,430],{"href":427,"rel":2577},[429],[11,2579,434,2580,440],{},[38,2581,439],{"href":437,"rel":2582},[429],[11,2584,443,2585,447,2587,453],{},[49,2586,446],{},[38,2588,452],{"href":450,"rel":2589},[429],[11,2591,456,2592,462],{},[38,2593,461],{"href":459,"rel":2594},[429],[15,2596,466],{"id":465},[11,2598,469,2599,473,2601,479],{},[49,2600,472],{},[38,2602,478],{"href":476,"rel":2603},[429],[11,2605,482,2606,485,2608,491],{},[49,2607,472],{},[38,2609,490],{"href":488,"rel":2610},[429],[11,2612,494],{},[15,2614,498],{"id":497},[11,2616,501,2617,507],{},[38,2618,506],{"href":504,"rel":2619},[429],[11,2621,510,2622,516],{},[38,2623,515],{"href":513,"rel":2624},[429],[11,2626,519],{},[11,2628,522,2629,526],{},[49,2630,525],{},[15,2632,530],{"id":529},[11,2634,533,2635,539,2638,545],{},[38,2636,538],{"href":536,"rel":2637},[429],[38,2639,544],{"href":542,"rel":2640},[429],[11,2642,548],{},[15,2644,552],{"id":551},[75,2646,2647,2659],{},[78,2648,2649],{},[81,2650,2651,2653,2655,2657],{},[84,2652,561],{},[84,2654,564],{},[84,2656,567],{},[84,2658,570],{},[91,2660,2661,2671,2683,2693],{},[81,2662,2663,2665,2667,2669],{},[96,2664,472],{},[96,2666,579],{},[96,2668,582],{},[96,2670,585],{},[81,2672,2673,2675,2677,2681],{},[96,2674,590],{},[96,2676,579],{},[96,2678,595,2679,598],{},[49,2680,525],{},[96,2682,601],{},[81,2684,2685,2687,2689,2691],{},[96,2686,606],{},[96,2688,609],{},[96,2690,612],{},[96,2692,615],{},[81,2694,2695,2697,2699,2701],{},[96,2696,620],{},[96,2698,623],{},[96,2700,626],{},[96,2702,585],{},[11,2704,631],{},[15,2706,635],{"id":634},[11,2708,638],{},[11,2710,641,2711,645,2713,649,2715,655],{},[49,2712,644],{},[49,2714,648],{},[38,2716,654],{"href":652,"rel":2717},[429],[11,2719,658],{},[15,2721,662],{"id":661},[11,2723,2724,669,2726,673,2728,677,2730,681],{},[38,2725,668],{"href":667},[49,2727,672],{},[49,2729,676],{},[49,2731,680],{},[11,2733,684],{},[11,2735,687,2736,690,2738,694,2740,698,2742,702],{},[49,2737,676],{},[49,2739,693],{},[49,2741,697],{},[49,2743,701],{},[11,2745,705],{},[15,2747,358],{"id":357},[357,2749],{":slug":710},{"title":363,"searchDepth":364,"depth":364,"links":2751},[2752,2753,2754,2755,2756,2757,2758,2759],{"id":420,"depth":364,"text":421},{"id":465,"depth":364,"text":466},{"id":497,"depth":364,"text":498},{"id":529,"depth":364,"text":530},{"id":551,"depth":364,"text":552},{"id":634,"depth":364,"text":635},{"id":661,"depth":364,"text":662},{"id":357,"depth":364,"text":358},[2761,2762,2763,2764,2765],{"q":725,"a":726},{"q":728,"a":729},{"q":731,"a":732},{"q":734,"a":735},{"q":737,"a":738},{},{"title":409,"description":722},[746,747,748,749,590,750],{"id":2770,"title":2771,"body":2772,"date":3137,"description":3138,"extension":376,"faq":3139,"image":3158,"lang":394,"meta":3159,"navigation":396,"path":3160,"published":396,"readTime":1270,"seo":3161,"stem":3162,"tags":3163,"updated":405,"__hash__":3169},"blog\u002Fblog\u002Fen\u002Fsubnet-calculator.md","How CIDR Slowed IPv4 Exhaustion and Routing Table Growth",{"type":8,"value":2773,"toc":3128},[2774,2781,2785,2794,2797,2806,2810,2838,2857,2879,2888,2894,2898,2916,2937,2941,2954,2963,2967,2970,3056,3065,3069,3082,3091,3098,3123,3125],[11,2775,2776,2777,2780],{},"An address such as ",[49,2778,2779],{},"192.168.1.0\u002F24"," looks like a compact configuration value, but the number after the slash was not invented merely for calculator convenience. It belongs to a reform that helped the internet survive its own growth. By the early 1990s, engineers needed both to allocate IPv4 space more efficiently and to restrain the global routing table, and those two problems relied on different properties of the same system.",[15,2782,2784],{"id":2783},"classes-worked-while-the-internet-was-small","Classes worked while the internet was small",[11,2786,2787,2788,2793],{},"In ",[38,2789,2792],{"href":2790,"rel":2791},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc791.html",[429],"RFC 791",", published in 1981, IPv4 networks had fixed formats. A class A network used 7 bits for its network number and 24 for its local part, class B used 14 and 16 bits respectively, and class C used 21 and 8. One class A block therefore contained 16,777,216 addresses, a class B block contained 65,536, and a class C block only 256.",[11,2795,2796],{},"That design left enormous gaps between the available sizes. A site with roughly 300 hosts had already outgrown one class C network. It could operate several class C networks, adding routing entries and operational complexity, or try to obtain a class B block in which most of the 65,536 addresses would remain unused. Address space was consumed unevenly while routers had to remember an increasing number of individual routes.",[11,2798,2799,2800,2805],{},"The scale of the second problem is visible in ",[38,2801,2804],{"href":2802,"rel":2803},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc1519.html",[429],"RFC 1519",". In January 1992, the default-free routing table held about 4,700 entries; by December it had grown to roughly 8,500. The document treated depletion of class B network numbers and routing information overload as immediate threats, with exhaustion of the entire IPv4 space following as a longer-term problem.",[15,2807,2809],{"id":2808},"cidr-addressed-two-problems-with-two-mechanisms","CIDR addressed two problems with two mechanisms",[11,2811,2812,2813,2818,2819,673,2822,677,2825,2828,2829,673,2832,1228,2835,507],{},"In September 1993, the IETF published ",[38,2814,2817],{"href":2815,"rel":2816},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc1518.html",[429],"RFC 1518"," on address allocation and aggregation architecture, along with RFC 1519 on Classless Inter-Domain Routing. Instead of choosing only among classes A, B, and C, a network could now be described by an address and prefix length. Prefixes such as ",[49,2820,2821],{},"\u002F13",[49,2823,2824],{},"\u002F21",[49,2826,2827],{},"\u002F27"," gained the same systematic meaning as the familiar ",[49,2830,2831],{},"\u002F8",[49,2833,2834],{},"\u002F16",[49,2836,2837],{},"\u002F24",[11,2839,2840,2841,2844,2845,2848,2849,2852,2853,2856],{},"For address allocation, this allowed a block to match demand more closely. An IPv4 prefix ",[49,2842,2843],{},"\u002Fp"," leaves ",[49,2846,2847],{},"32 - p"," variable bits, so the block contains ",[49,2850,2851],{},"2^(32 - p)"," addresses. CIDR does not, however, let an operator request literally any number: the size is always a power of two, and the block must start on its prefix boundary. A conventional network for 300 hosts can use a ",[49,2854,2855],{},"\u002F23",", which contains 512 addresses in total and 510 after network and broadcast are excluded. That is much closer to the requirement than an entire class B block.",[11,2858,2859,2860,2863,2864,2867,2868,2871,2872,2863,2875,2878],{},"The second mechanism is route aggregation. If sixteen adjacent networks from ",[49,2861,2862],{},"10.24.0.0\u002F24"," through ",[49,2865,2866],{},"10.24.15.0\u002F24"," share the same path and routing policy, an organization can represent them internally with the single route ",[49,2869,2870],{},"10.24.0.0\u002F20",". Their first 20 bits are identical, while the next four bits run from ",[49,2873,2874],{},"0000",[49,2876,2877],{},"1111",". An upstream router needs one summary route instead of sixteen details.",[11,2880,2881,2882,2887],{},"This example uses private address space and applies only to internal routing. ",[38,2883,2886],{"href":2884,"rel":2885},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc1918.html",[429],"RFC 1918"," explicitly says that such routes must not propagate between enterprises. Global aggregation follows the same bitwise rule, but it requires aligned public blocks with common reachability and policy. If part of a block moves to another provider, a more specific route is still needed, so CIDR slowed routing-table growth rather than stopping it.",[11,2889,2890],{},[1068,2891],{"alt":2892,"src":2893},"The 192.0.2.0\u002F24 network splits into four \u002F26 subnets while sixteen internal 10.24.0.0\u002F24 routes aggregate into 10.24.0.0\u002F20","\u002Fimages\u002Fblog\u002Finline_subnet-calculator.webp",[15,2895,2897],{"id":2896},"what-a-prefix-length-actually-means","What a prefix length actually means",[11,2899,1023,2900,2902,2903,2906,2907,2910,2911,2913,2914,507],{},[49,2901,2837],{}," leaves 8 bits for addresses and therefore contains 256 values. A ",[49,2904,2905],{},"\u002F26"," leaves 6 bits and contains 64. In conventional IPv4 subnets through ",[49,2908,2909],{},"\u002F30",", the first value identifies the network and the last is broadcast, leading to the familiar counts of 254 usable addresses in a ",[49,2912,2837],{}," and 62 in a ",[49,2915,2905],{},[11,2917,2918,2919,2924,2925,2928,2929,2932,2933,2936],{},"The edge of this arithmetic has important special cases. ",[38,2920,2923],{"href":2921,"rel":2922},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc3021.html",[429],"RFC 3021"," permits both addresses of a ",[49,2926,2927],{},"\u002F31"," on a point-to-point link when both endpoints support that convention. A ",[49,2930,2931],{},"\u002F32"," identifies exactly one IPv4 address and is commonly used as a host route. At the other extreme, ",[49,2934,2935],{},"\u002F0"," covers the entire IPv4 address space and serves as the default route, but that does not mean every address inside it is assignable or globally routable.",[15,2938,2940],{"id":2939},"private-addresses-and-nat-are-not-part-of-cidr","Private addresses and NAT are not part of CIDR",[11,2942,2943,2944,673,2947,1228,2950,2953],{},"In 1996, RFC 1918 reserved three ranges for private networks: ",[49,2945,2946],{},"10.0.0.0\u002F8",[49,2948,2949],{},"172.16.0.0\u002F12",[49,2951,2952],{},"192.168.0.0\u002F16",". Independent homes and organizations can reuse them, while routes to those addresses must not be carried across the public internet.",[11,2955,2956,2957,2962],{},"NAT is often discussed beside these ranges because address translation lets many internal devices reach external networks through a smaller set of public addresses. It is nevertheless a separate mechanism whose early design appeared in ",[38,2958,2961],{"href":2959,"rel":2960},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc1631.html",[429],"RFC 1631"," in 1994. CIDR, private addressing, and NAT collectively reduced pressure on IPv4, but attributing the whole result to one RFC would be inaccurate.",[15,2964,2966],{"id":2965},"exhaustion-happened-in-stages","Exhaustion happened in stages",[11,2968,2969],{},"The phrase “IPv4 ran out” compresses several different events. IANA distributes large blocks to regional internet registries, which allocate smaller blocks to network operators. There can therefore be no single exhaustion date:",[298,2971,2972,2986,2999,3016,3030,3040],{},[301,2973,2974,2975,2982,2983,2985],{},"On ",[38,2976,2979],{"href":2977,"rel":2978},"https:\u002F\u002Fwww.arin.net\u002Fvault\u002Fresources\u002Fguide\u002Frequest\u002Fipv4_depletion\u002F",[429],[26,2980,2981],{},"3 February 2011",", IANA assigned its final five free ",[49,2984,2831],{}," blocks, one to each regional registry.",[301,2987,2974,2988,2995,2996,2998],{},[38,2989,2992],{"href":2990,"rel":2991},"https:\u002F\u002Fconference.apnic.net\u002Fnews-archives\u002F2011\u002Ffinal-8\u002F",[429],[26,2993,2994],{},"15 April 2011",", APNIC entered its final ",[49,2997,2831],{}," policy and sharply limited new allocations.",[301,3000,2974,3001,3004,3005,3007,3008,3015],{},[26,3002,3003],{},"14 September 2012",", the RIPE NCC began allocating from its final ",[49,3006,2831],{},"; on ",[38,3009,3012],{"href":3010,"rel":3011},"https:\u002F\u002Fwww.ripe.net\u002Fpublications\u002Fnews\u002Fabout-ripe-ncc-and-ripe\u002Fthe-ripe-ncc-has-run-out-of-ipv4-addresses\u002F",[429],[26,3013,3014],{},"25 November 2019",", it announced depletion of its remaining free pool.",[301,3017,2974,3018,3021,3022,3029],{},[26,3019,3020],{},"10 June 2014",", LACNIC entered a restricted allocation phase; on ",[38,3023,3026],{"href":3024,"rel":3025},"https:\u002F\u002Fwww.lacnic.net\u002F4848\u002F2\u002Flacnic\u002Fipv4-exhaustion%3A-lacnic-has-assigned-the-last-remaining-address-block",[429],[26,3027,3028],{},"19 August 2020",", it reported assigning its last available reserve block.",[301,3031,2974,3032,3039],{},[38,3033,3036],{"href":3034,"rel":3035},"https:\u002F\u002Fwww.arin.net\u002Fvault\u002Fabout\u002Fmedia\u002Freleases\u002F20150924\u002F",[429],[26,3037,3038],{},"24 September 2015",", ARIN announced depletion of its IPv4 free pool.",[301,3041,3042,3043,3048,3049,3052,3053,507],{},"According to ",[38,3044,3047],{"href":3045,"rel":3046},"https:\u002F\u002Fweb10.mba.afrinic.net\u002Fexhaustion",[429],"AFRINIC's exhaustion timeline",", the region entered its first phase on ",[26,3050,3051],{},"31 March 2017"," and its second, more restrictive phase on ",[26,3054,3055],{},"13 January 2020",[11,3057,3058,3059,3064],{},"These milestones changed allocation policy; they did not switch off addresses already in use. Operators continue to use existing blocks, obtain resources through transfers, recover or subdivide smaller ranges, and deploy IPv6. The current base IPv6 specification, ",[38,3060,3063],{"href":3061,"rel":3062},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc8200.html",[429],"RFC 8200",", uses 128-bit addresses, but migration requires support across every part of a path. IPv4, dual stack, and translation mechanisms consequently continue to coexist.",[15,3066,3068],{"id":3067},"what-subnet-calculator-reports","What Subnet Calculator reports",[11,3070,510,3071,3075,3076,3078,3079,3081],{},[38,3072,3074],{"href":3073},"\u002Ftools\u002Fsubnet-calculator","Subnet Calculator"," performs its arithmetic locally in the browser. For IPv4, it reports the network address, broadcast, first and last host addresses, netmask, wildcard, total and usable counts, and a binary representation with the prefix boundary. For a ",[49,3077,2927],{},", it counts both addresses as usable and omits broadcast; for a ",[49,3080,2931],{},", it returns one address.",[11,3083,3084,3085,2863,3087,3090],{},"In IPv6 mode, the calculator accepts conventional hexadecimal notation with a prefix from ",[49,3086,2935],{},[49,3088,3089],{},"\u002F128",". It computes the first and last address in the block, presents compressed and expanded forms, and reports the host-bit count and exact range size. IPv6 has no broadcast equivalent to IPv4, so no broadcast field appears.",[11,3092,3093,3094,3097],{},"The calculator does not decide whether a selected public block is actually obtainable, whether an address belongs to a special-purpose range, or whether a route is valid in a particular network. It also does not check routing policy, overlapping subnets, or the usual IPv6 choice of ",[49,3095,3096],{},"\u002F64"," for a LAN segment. Its narrower job is more useful: it shows the exact boundaries of a prefix so they can be verified before configuring DHCP, a firewall, or a router.",[11,3099,3100,3101,3104,3105,3108,3109,3112,3113,2863,3116,3119,3120,3122],{},"To see the arithmetic directly, enter ",[49,3102,3103],{},"192.0.2.130\u002F26",". The address belongs to ",[49,3106,3107],{},"192.0.2.128\u002F26",", whose range ends at ",[49,3110,3111],{},"192.0.2.191","; its conventional host addresses run from ",[49,3114,3115],{},"192.0.2.129",[49,3117,3118],{},"192.0.2.190",". Then change the prefix to ",[49,3121,2927],{}," and compare the result. The two values become point-to-point endpoint addresses, and the broadcast field disappears.",[15,3124,358],{"id":357},[357,3126],{":slug":3127},"[\"subnet-calculator\",\"epoch-counter\",\"cron-parser\",\"api-sandbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":3129},[3130,3131,3132,3133,3134,3135,3136],{"id":2783,"depth":364,"text":2784},{"id":2808,"depth":364,"text":2809},{"id":2896,"depth":364,"text":2897},{"id":2939,"depth":364,"text":2940},{"id":2965,"depth":364,"text":2966},{"id":3067,"depth":364,"text":3068},{"id":357,"depth":364,"text":358},"2026-08-22","How CIDR replaced classful addressing, when route aggregation works, and what the special IPv4 prefixes \u002F31, \u002F32, and \u002F0 actually mean.",[3140,3143,3146,3149,3152,3155],{"q":3141,"a":3142},"What is CIDR and why was it invented?","CIDR, or Classless Inter-Domain Routing, arrived in 1993 as an answer to two problems at once: wasteful IPv4 allocation and rapid growth of the global routing table. It replaced fixed network classes with variable-length prefixes and made it possible to aggregate suitable neighboring routes.",{"q":3144,"a":3145},"Can CIDR create a block of any size?","No. A prefix can have any length from \u002F0 through \u002F32, but the block always contains a power-of-two number of addresses and must begin on the matching prefix boundary. For 300 conventional IPv4 hosts, a \u002F23 is suitable: it contains 512 total addresses and 510 after excluding network and broadcast.",{"q":3147,"a":3148},"What does 192.168.1.0\u002F24 mean?","The number after the slash is the network prefix length. In a \u002F24, the first 24 bits identify the network and the remaining 8 vary within the block, producing 256 addresses. A conventional subnet reserves two of them for network and broadcast, while \u002F31 and \u002F32 follow separate rules.",{"q":3150,"a":3151},"Why does a \u002F31 have no network and broadcast address?","RFC 3021 permits both addresses in a \u002F31 to identify the endpoints of a point-to-point link, where there are exactly two parties and broadcast is unnecessary. This halves the address cost compared with a \u002F30, but it should be used only when both endpoints support the convention.",{"q":3153,"a":3154},"Are RFC 1918 and NAT part of CIDR?","No. CIDR defines classless addressing and route aggregation, RFC 1918 defines private IPv4 ranges, and NAT translates addresses at a network boundary. They are often deployed together, but they solve different problems and were specified in different documents.",{"q":3156,"a":3157},"Why does the internet still work after free IPv4 exhaustion?","Exhaustion means a regional registry can no longer make ordinary large allocations from its former free pool. Previously allocated addresses did not disappear, and networks can also use transfers, small recovered blocks, private addressing with NAT, and IPv6 deployment.","\u002Fimages\u002Fblog\u002Fblog_subnet-calculator.webp",{},"\u002Fblog\u002Fen\u002Fsubnet-calculator",{"title":2771,"description":3138},"blog\u002Fen\u002Fsubnet-calculator",[3164,3165,3166,3167,3168,2804],"CIDR history","why CIDR was invented","IPv4 address exhaustion","classful addressing","classless inter-domain routing","8FzsQDqWvnNIyEIdgIKKWoIT5cIWFT_5ZQqkRm2Udfc",{"id":3171,"title":3172,"body":3173,"date":3360,"description":3361,"extension":376,"faq":3362,"image":3378,"lang":394,"meta":3379,"navigation":396,"path":3380,"published":396,"readTime":1270,"seo":3381,"stem":3382,"tags":3383,"updated":405,"__hash__":3390},"blog\u002Fblog\u002Fen\u002Fcron-parser.md","How Cron's Five Fields Outlived Generations of Unix",{"type":8,"value":3174,"toc":3351},[3175,3178,3182,3195,3203,3206,3210,3219,3222,3226,3233,3240,3253,3256,3262,3266,3273,3276,3279,3283,3291,3299,3316,3320,3327,3337,3343,3346,3348],[11,3176,3177],{},"Cron is often described as one old Unix program that has survived unchanged. What actually endured is mostly its language: five calendar fields are still recognizable in Linux, hosting control panels, Kubernetes, and continuous integration systems, even though different programs execute those schedules under different rules.",[15,3179,3181],{"id":3180},"cron-predates-unix-version-7","Cron predates Unix Version 7",[11,3183,3184,3185,3190,3191,3194],{},"Accounts of cron often begin with Unix Version 7 and 1979, but surviving ",[38,3186,3189],{"href":3187,"rel":3188},"https:\u002F\u002Fwww.retro11.de\u002Fouxr\u002Fu6ed\u002Fusr\u002Fsource\u002Fs1\u002Fcron.c.html",[429],"Unix Sixth Edition source code"," is dated May 1975. That program read ",[49,3192,3193],{},"\u002Fusr\u002Flib\u002Fcrontab",", parsed five time fields, and passed the rest of each matching line to the shell. Asterisks, lists, and ranges were already supported, and the schedule was checked once per minute.",[11,3196,510,3197,3202],{},[38,3198,3201],{"href":3199,"rel":3200},"https:\u002F\u002Fbitsavers.trailing-edge.com\u002Fpdf\u002Fatt\u002Funix\u002F7th_Edition\u002FUNIX_Programmers_Manual_Seventh_Edition_Vol_1_1983.pdf",[429],"Unix Version 7 manual"," documented the interface clearly: minute, hour, day of month, month, day of week, and a string for the shell. The field order survived, although individual ranges and extensions changed over time. V7, for example, numbered weekdays from 1 through 7 with Monday first, while many current implementations use 0 for Sunday and may accept 7 as well.",[11,3204,3205],{},"The historical source does not provide enough evidence to name the author of the first version with confidence. The useful, verifiable conclusion is narrower: cron existed by 1975, and its recognizable interface was documented as part of Unix by 1979.",[15,3207,3209],{"id":3208},"vixie-cron-established-an-influential-open-lineage","Vixie cron established an influential open lineage",[11,3211,3212,3213,3218],{},"Paul Vixie began his cron implementation in January 1987. Its ",[38,3214,3217],{"href":3215,"rel":3216},"https:\u002F\u002Fgithub.com\u002Fvixie\u002Fcron",[429],"project repository"," explicitly says that it was functionally based on System V, so describing it as a clean-sheet rewrite would be misleading. Per-user schedule tables were not a new invention at that point either.",[11,3220,3221],{},"Vixie cron mattered for a different reason. It was a freely distributed, portable implementation of a compatible interface, bringing together per-user crontabs in protected storage, management commands, allow and deny lists, environment settings, logging, and mail delivery of output. Its format stayed close to V7 while adding compatible features such as steps and names for months and weekdays. The code later became an ancestor of several BSD and Linux cron variants, although the exact feature set still depends on the implementation installed on a particular system.",[15,3223,3225],{"id":3224},"five-fields-describe-a-calendar-match","Five fields describe a calendar match",[11,3227,3228,3229,3232],{},"The expression ",[49,3230,3231],{},"30 4 * * 1-5"," means 04:30 from Monday through Friday. The first field is the minute, the second is the hour, followed by day of month, month, and day of week. An asterisk permits every value in a field, a comma combines values, a hyphen creates a range, and a slash selects every nth value within a field or range.",[11,3234,3235,3236,3239],{},"A step applies only within its field. ",[49,3237,3238],{},"*\u002F15"," in the minute position means minutes 0, 15, 30, and 45 of every matching hour, not a fifteen-minute interval measured from the previous execution. The distinction becomes clear after downtime: ordinary cron does not keep a queue of those intervals; it simply asks whether the current calendar minute matches the expression.",[11,3241,3242,3243,3248,3249,3252],{},"There is another rule that regularly causes surprises. In the ",[38,3244,3247],{"href":3245,"rel":3246},"https:\u002F\u002Fgithub.com\u002Fvixie\u002Fcron\u002Fblob\u002Fmaster\u002Fcrontab.5",[429],"Vixie cron format",", restricting both day of month and day of week produces a run when either field matches. Thus ",[49,3250,3251],{},"0 9 1 * 1"," does not mean only a Monday that falls on the first. It means every Monday and every first day of the month at 09:00. Other scheduler families can interpret similar-looking input differently.",[11,3254,3255],{},"The expression itself describes only a calendar condition. It does not include the command, working directory, environment, retry policy, concurrency limit, or log destination. Those details belong to the scheduler and its configuration.",[11,3257,3258],{},[1068,3259],{"alt":3260,"src":3261},"Anatomy of the five cron expression fields, weekly calendar schedule grid, and operational considerations","\u002Fimages\u002Fblog\u002Finline_cron-parser.webp",[15,3263,3265],{"id":3264},"a-schedule-does-not-define-execution-conditions","A schedule does not define execution conditions",[11,3267,3268,3269,3272],{},"A cron job does not run in the same environment as a command entered in an interactive terminal. It usually receives fewer variables, its ",[49,3270,3271],{},"PATH"," may differ, and the user's shell profile is normally not loaded. A script can therefore work by hand and fail on schedule. A reliable job uses a predictable environment, absolute paths where they are genuinely required, and a place to preserve diagnostic output.",[11,3274,3275],{},"Wall-clock time needs an explicit decision too. The time zone may come from the system, the daemon, or the table itself, while daylight-saving changes create skipped and repeated local minutes. Even current cron implementations differ in how they handle clock changes, so there is no universal answer implied by the word “cron.” Check the local manual and decide whether a duplicate execution would be safe.",[11,3277,3278],{},"Traditional cron also does not recover work missed while a computer was off. Periodic jobs on a machine that does not run continuously often use anacron or another stateful scheduler. If execution may last longer than its interval, preventing overlap and making the operation idempotent remain responsibilities of the job or an external locking mechanism.",[15,3280,3282],{"id":3281},"kubernetes-ci-and-systemd-inherited-different-parts-of-the-idea","Kubernetes, CI, and systemd inherited different parts of the idea",[11,3284,1023,3285,3290],{},[38,3286,3289],{"href":3287,"rel":3288},"https:\u002F\u002Fkubernetes.io\u002Fdocs\u002Fconcepts\u002Fworkloads\u002Fcontrollers\u002Fcron-jobs\u002F",[429],"Kubernetes CronJob"," uses familiar schedule notation, but it creates Job objects rather than invoking a shell command directly. The controller schedules approximately, and under some conditions it may create two Jobs or none, which is why the documentation recommends idempotent workloads. Concurrency policy, the deadline for a late start, and the time zone are configured separately.",[11,3292,3293,3298],{},[38,3294,3297],{"href":3295,"rel":3296},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\u002Freference\u002Fworkflows-and-actions\u002Fevents-that-trigger-workflows#schedule",[429],"GitHub Actions"," also borrows POSIX-like syntax, but it imposes a minimum interval, runs the workflow from the default branch, and warns that scheduled runs can be delayed or queued jobs dropped during heavy load. The five fields express intent; the hosted service supplies the actual guarantees.",[11,3300,3301,3302,3307,3308,3311,3312,3315],{},"Systemd timers are a separate mechanism, not a newer spelling of cron. A ",[38,3303,3306],{"href":3304,"rel":3305},"https:\u002F\u002Fgithub.com\u002Fsystemd\u002Fsystemd\u002Fblob\u002Fmain\u002Fman\u002Fsystemd.timer.xml",[429],"systemd timer"," activates a related service unit, supports both calendar and monotonic timers, and can recover one missed calendar activation when ",[49,3309,3310],{},"Persistent=true"," is enabled. If the related service is already active, another timer event does not restart it automatically. Mechanically converting a cron expression into ",[49,3313,3314],{},"OnCalendar="," therefore does not make the two configurations equivalent.",[15,3317,3319],{"id":3318},"what-cron-parser-does","What Cron Parser does",[11,3321,3322,3326],{},[38,3323,3325],{"href":3324},"\u002Ftools\u002Fcron-parser","Cron Parser"," parses a five-field expression locally in the browser. You can fill the fields separately or paste the complete expression into any one of them. It understands numbers, asterisks, lists, ranges, steps, and English names for months and weekdays. Sunday value 7 is normalized to 0, while a question mark in the day-of-month or day-of-week field is treated as an asterisk. The latter is an extension and is not portable to every scheduler.",[11,3328,3329,3330,698,3333,3336],{},"After parsing, the tool explains each field, visualizes the matching hours and minutes for an eligible calendar day, and calculates up to ten upcoming occurrences in the browser's local time or UTC. The search window is finite, so a rare annual schedule can sometimes produce fewer than ten dates. Preset buttons expand common macros into their five-field equivalents, but raw input such as ",[49,3331,3332],{},"@daily",[49,3334,3335],{},"@weekly"," is not accepted as an expression.",[11,3338,3339,3340,3342],{},"The systemd timer generator is only a starting template for simple schedules. It creates timer sections but not the service unit or command, and complex constraints involving days, months, lists, and ranges cannot always be translated faithfully. Its added ",[49,3341,3310],{}," also changes behavior after downtime. Review and complete the result manually before using it.",[11,3344,3345],{},"Cron Parser installs and runs nothing. The data stays in the browser, and the resulting schedule still needs to be checked against the target system's documentation.",[15,3347,358],{"id":357},[357,3349],{":slug":3350},"[\"cron-parser\",\"epoch-counter\",\"subnet-calculator\",\"api-sandbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":3352},[3353,3354,3355,3356,3357,3358,3359],{"id":3180,"depth":364,"text":3181},{"id":3208,"depth":364,"text":3209},{"id":3224,"depth":364,"text":3225},{"id":3264,"depth":364,"text":3265},{"id":3281,"depth":364,"text":3282},{"id":3318,"depth":364,"text":3319},{"id":357,"depth":364,"text":358},"2026-08-21","The history of cron from 1975 Unix to modern schedulers, how its expressions work, and why schedule syntax does not define actual execution behavior.",[3363,3366,3369,3372,3375],{"q":3364,"a":3365},"When was cron created, and who wrote it?","Cron source code already appears in the 1975 Unix Sixth Edition. The surviving source does not identify an author, so attributing the first version to a particular person would require additional evidence. Unix Version 7, released in 1979, documented an interface that already looks much like modern cron.",{"q":3367,"a":3368},"What did Vixie cron change?","Vixie cron began in 1987 as a freely distributed, portable implementation functionally based on System V. It did not reinvent cron from scratch, but it established an influential line of development with per-user crontabs, access controls, environment settings, and compatible extensions. Several BSD and Linux variants descend from that code.",{"q":3370,"a":3371},"What do the five cron fields mean?","The fields specify minute, hour, day of month, month, and day of week. A command normally follows them in a user crontab, while a system crontab may place a user name between the schedule and command. In the Vixie family, restricted day-of-month and day-of-week fields are combined with OR semantics.",{"q":3373,"a":3374},"What happens during daylight-saving changes or server downtime?","The answer depends on the implementation. Traditional cron checks the calendar once per minute and generally does not recover runs missed while the system was down. Clock-change behavior also varies, so it must be checked in the documentation for the installed cron. Anacron, systemd timers, and the Kubernetes CronJob controller each handle missed runs differently.",{"q":3376,"a":3377},"Does Cron Parser validate a schedule for every platform?","No. It parses a common five-field form and previews the schedule, but Cronie, Quartz, Kubernetes, GitHub Actions, and other systems have their own extensions and limits. Check the target platform's documentation, especially when using a question mark, macros, time zones, or the systemd timer output.","\u002Fimages\u002Fblog\u002Fblog_cron-parser.webp",{},"\u002Fblog\u002Fen\u002Fcron-parser",{"title":3172,"description":3361},"blog\u002Fen\u002Fcron-parser",[3384,3385,3386,3387,3388,3389],"cron history","Vixie cron","how UNIX cron works","who created cron","cron vs systemd timer","crontab syntax","v7o39uZZzp3d54VldQvzvwrpCKegXChAgmYbf0jLyEA",{"id":3392,"title":3393,"body":3394,"date":3360,"description":3529,"extension":376,"faq":3530,"image":3537,"lang":394,"meta":3538,"navigation":396,"path":3539,"published":396,"readTime":364,"seo":3540,"stem":3541,"tags":3542,"updated":405,"__hash__":3547},"blog\u002Fblog\u002Fen\u002Fhttp-status-codes-api-contract.md","HTTP Status Codes: Build Predictable API Contracts",{"type":8,"value":3395,"toc":3522},[3396,3399,3403,3406,3430,3437,3441,3454,3465,3469,3490,3505,3509,3517,3519],[11,3397,3398],{},"HTTP status codes are part of an API contract, not just diagnostic text for browser developer tools. Clients branch on them, caches interpret them, gateways retry some of them, and monitoring uses them to distinguish a bad request from a failing dependency.",[15,3400,3402],{"id":3401},"start-with-the-outcome","Start with the outcome",[11,3404,3405],{},"For a conventional resource endpoint, keep the happy paths boring:",[298,3407,3408,3418,3424],{},[301,3409,3410,3413,3414,3417],{},[49,3411,3412],{},"201 Created"," after creating a resource, ideally with a ",[49,3415,3416],{},"Location"," header;",[301,3419,3420,3423],{},[49,3421,3422],{},"200 OK"," when a read or update returns a representation;",[301,3425,3426,3429],{},[49,3427,3428],{},"204 No Content"," when an update or delete has no useful body to return.",[11,3431,3432,3433,3436],{},"Avoid returning ",[49,3434,3435],{},"200"," for every operation with an error object hidden in JSON. That makes HTTP-aware clients, caches, observability, and generic integrations less useful.",[15,3438,3440],{"id":3439},"separate-request-failure-from-domain-failure","Separate request failure from domain failure",[11,3442,3443,3446,3447,3450,3451,507],{},[49,3444,3445],{},"400 Bad Request"," is for malformed syntax: invalid JSON, an impossible header, or a request that cannot be parsed. ",[49,3448,3449],{},"422 Unprocessable Content"," is for a correctly parsed request whose fields or business rules are invalid. A duplicate unique value, an outdated version, or another conflict with the current resource state is a better fit for ",[49,3452,3453],{},"409 Conflict",[11,3455,3456,3457,3460,3461,3464],{},"Authentication also has a useful split. Return ",[49,3458,3459],{},"401 Unauthorized"," when credentials are missing or invalid; return ",[49,3462,3463],{},"403 Forbidden"," after identifying the caller but refusing the action. Keeping those meanings stable prevents clients from prompting for a new login when the real issue is a missing role.",[15,3466,3468],{"id":3467},"make-temporary-conditions-actionable","Make temporary conditions actionable",[11,3470,3471,3472,1121,3475,3478,3479,3482,3483,3486,3487,507],{},"Clients can respond safely to a temporary condition only if the server states it clearly. ",[49,3473,3474],{},"429 Too Many Requests",[49,3476,3477],{},"503 Service Unavailable"," should include ",[49,3480,3481],{},"Retry-After"," whenever you know the delay. A reverse proxy that receives an invalid upstream reply should use ",[49,3484,3485],{},"502 Bad Gateway","; one that waits too long should use ",[49,3488,3489],{},"504 Gateway Timeout",[11,3491,3492,3493,3496,3497,3500,3501,3504],{},"For conditional writes, require an ",[49,3494,3495],{},"If-Match"," header and answer ",[49,3498,3499],{},"428 Precondition Required"," when it is absent. If the supplied ETag no longer matches, return ",[49,3502,3503],{},"412 Precondition Failed",". This turns accidental last-write-wins behavior into an explicit, recoverable conflict.",[15,3506,3508],{"id":3507},"keep-a-shared-reference-close","Keep a shared reference close",[11,3510,3511,3512,3516],{},"Use the ",[38,3513,3515],{"href":3514},"\u002Ftools\u002Fhttp-status-codes","HTTP Status Codes tool"," to search the registered codes, review related responses, copy handling patterns for six languages, and inspect a response from a CORS-enabled endpoint directly in the browser.",[15,3518,358],{"id":357},[357,3520],{":slug":3521},"[\"http-status-codes\",\"api-sandbox\",\"webhook-verifier\"]",{"title":363,"searchDepth":364,"depth":364,"links":3523},[3524,3525,3526,3527,3528],{"id":3401,"depth":364,"text":3402},{"id":3439,"depth":364,"text":3440},{"id":3467,"depth":364,"text":3468},{"id":3507,"depth":364,"text":3508},{"id":357,"depth":364,"text":358},"Choose meaningful HTTP responses for CRUD, validation, authentication, caching, rate limits, and upstream failures.",[3531,3534],{"q":3532,"a":3533},"Should validation errors be 400 or 422?","Use 400 when the request cannot be parsed or is malformed. Use 422 when its syntax is valid but individual values or business rules fail validation.",{"q":3535,"a":3536},"What is the difference between 401 and 403?","401 means the request lacks valid credentials. 403 means the credentials are valid but do not grant permission for the requested action.","\u002Fimages\u002Fblog\u002Fblog_http-status-codes-api-contract.webp",{},"\u002Fblog\u002Fen\u002Fhttp-status-codes-api-contract",{"title":3393,"description":3529},"blog\u002Fen\u002Fhttp-status-codes-api-contract",[3543,3544,3545,3546],"HTTP","API design","REST","Error handling","uz4V_3nR1FjVLAlnH6BY9FyTHiM1eeW35vCsYUKgBPU",{"id":3549,"title":3550,"body":3551,"date":3876,"description":3877,"extension":376,"faq":3878,"image":3899,"lang":394,"meta":3900,"navigation":396,"path":3901,"published":396,"readTime":1270,"seo":3902,"stem":3903,"tags":3904,"updated":405,"__hash__":3909},"blog\u002Fblog\u002Fen\u002Fyaml-json-converter.md","JSON, YAML, and TOML: Why the Same Data Behaves Differently",{"type":8,"value":3552,"toc":3868},[3553,3568,3572,3588,3594,3597,3601,3624,3627,3655,3683,3708,3731,3734,3738,3769,3775,3779,3782,3795,3799,3812,3838,3844,3857,3860,3862,3865],[11,3554,3555,3556,3559,3560,3563,3564,3567],{},"The line ",[49,3557,3558],{},"country: NO"," looks too straightforward to contain a surprise. An older YAML parser, however, may read ",[49,3561,3562],{},"NO"," as the boolean ",[49,3565,3566],{},"false",", while JSON would require quotation marks and TOML would distinguish the string from a boolean explicitly. This is more than one unfortunate syntax choice: the three formats have different data models, goals, and type-resolution rules. Choosing among them therefore begins with who will write the file, which program will read it, and how much ambiguity the exchange can tolerate.",[15,3569,3571],{"id":3570},"json-separated-data-from-executable-code","JSON separated data from executable code",[11,3573,3574,3575,3579,3580,3583,3584,3587],{},"JSON grew out of JavaScript object literals, but it has no single moment of invention. ",[38,3576,3578],{"href":1620,"rel":3577},[429],"Douglas Crockford recalled"," that several developers independently used this notation for data exchange and that he arrived at his own version at State Software in April 2001. He registered ",[49,3581,3582],{},"json.org"," in 2002 and began documenting the notation as a language-independent format. RFC 4627 followed in 2006, ECMA-404 in 2013, and the current ",[38,3585,1702],{"href":1700,"rel":3586},[429]," in 2017.",[11,3589,3590,3591,3593],{},"JSON's strength is its small, predictable model. A value can be a string, number, boolean, ",[49,3592,1741],{},", array, or object whose member names are strings. That model maps easily into many programming languages, which makes JSON a natural fit for APIs, event logs, and data primarily created and consumed by software.",[11,3595,3596],{},"Minimalism also sets boundaries. The JSON grammar has no comments or distinct date type, and large numbers can lose precision in systems that store them as ordinary 64-bit floating-point values. The standard also recommends unique object member names: when duplicates occur, one parser may retain the last value, another may report an error, and another may expose every pair. A short grammar does not remove the need to agree on semantics above it.",[15,3598,3600],{"id":3599},"yaml-favored-authoring-convenience-and-gained-several-ways-to-resolve-a-scalar","YAML favored authoring convenience and gained several ways to resolve a scalar",[11,3602,3603,3604,3609,3610,3613,3614,3617,3618,3623],{},"Clark Evans first proposed YAML in 2001 and designed it together with Ingy döt Net and Oren Ben-Kiki. An ",[38,3605,3608],{"href":3606,"rel":3607},"https:\u002F\u002Fyaml.org\u002Fspec\u002Fhistory\u002F2001-12-10.html",[429],"early specification draft"," expanded the name as ",[1321,3611,3612],{},"Yet Another Markup Language",". The modern recursive name, ",[1321,3615,3616],{},"YAML Ain't Markup Language",", emphasizes that YAML is a data serialization language rather than document markup. The ",[38,3619,3622],{"href":3620,"rel":3621},"https:\u002F\u002Fyaml.org\u002Fabout\u002F",[429],"official YAML history"," records both the authorship and the current meaning of the name.",[11,3625,3626],{},"The format was designed for people to read and edit. Indentation expresses nesting, comments explain settings, block scalars hold multiline text, and anchors and aliases can reuse nodes. Those facilities are valuable in substantial configuration files, but they also make YAML much broader than a simple collection of key-value pairs.",[11,3628,3629,3630,3635,3636,1121,3639,3641,3642,673,3645,673,3648,1228,3651,3654],{},"The Norway Problem came from this type system. The ",[38,3631,3634],{"href":3632,"rel":3633},"https:\u002F\u002Fyaml.org\u002Ftype\u002Fbool.html",[429],"YAML 1.1 boolean schema"," recognized not only ",[49,3637,3638],{},"true",[49,3640,3566],{},", but variants of ",[49,3643,3644],{},"yes",[49,3646,3647],{},"no",[49,3649,3650],{},"on",[49,3652,3653],{},"off"," as well. The following document could therefore change meaning without producing a syntax error:",[1564,3656,3660],{"className":3657,"code":3658,"language":3659,"meta":363,"style":363},"language-yaml shiki shiki-themes github-dark","country: NO\nmaintenance: ON\n","yaml",[49,3661,3662,3673],{"__ignoreMap":363},[1572,3663,3664,3668,3670],{"class":1574,"line":1575},[1572,3665,3667],{"class":3666},"s4JwU","country",[1572,3669,1648],{"class":1638},[1572,3671,3672],{"class":1644},"NO\n",[1572,3674,3675,3678,3680],{"class":1574,"line":364},[1572,3676,3677],{"class":3666},"maintenance",[1572,3679,1648],{"class":1638},[1572,3681,3682],{"class":1644},"ON\n",[11,3684,3685,3686,1121,3688,3690,3691,3696,3697,1121,3699,3701,3702,1121,3704,3707],{},"Under the YAML 1.1 schema, those values could resolve to ",[49,3687,3566],{},[49,3689,3638],{},". In ",[38,3692,3695],{"href":3693,"rel":3694},"https:\u002F\u002Fyaml.org\u002Fspec\u002F1.2.2\u002F",[429],"YAML 1.2 Core Schema",", only variants of ",[49,3698,3638],{},[49,3700,3566],{}," resolve as booleans, leaving ",[49,3703,3562],{},[49,3705,3706],{},"ON"," as strings. The version number alone does not guarantee the outcome, however, because a library may implement an older version, select another schema, or add its own behavior. Quotation marks state the intended type directly when the value must remain a string everywhere:",[1564,3709,3711],{"className":3657,"code":3710,"language":3659,"meta":363,"style":363},"country: \"NO\"\nmaintenance: \"ON\"\n",[49,3712,3713,3722],{"__ignoreMap":363},[1572,3714,3715,3717,3719],{"class":1574,"line":1575},[1572,3716,3667],{"class":3666},[1572,3718,1648],{"class":1638},[1572,3720,3721],{"class":1651},"\"NO\"\n",[1572,3723,3724,3726,3728],{"class":1574,"line":364},[1572,3725,3677],{"class":3666},[1572,3727,1648],{"class":1638},[1572,3729,3730],{"class":1651},"\"ON\"\n",[11,3732,3733],{},"This does not prove that YAML is inherently bad. It demonstrates the cost of implicit type resolution and explains why parser behavior belongs in the configuration contract.",[15,3735,3737],{"id":3736},"toml-made-configuration-its-primary-job","TOML made configuration its primary job",[11,3739,3740,3741,3746,3747,3750,3751,3754,3755,3758,3759,698,3761,3763,3764,507],{},"Tom Preston-Werner began TOML in 2013 as a format with obvious semantics that ",[38,3742,3745],{"href":3743,"rel":3744},"https:\u002F\u002Fgithub.com\u002Ftoml-lang\u002Ftoml",[429],"maps unambiguously to a hash table",". Instead of meaningful indentation, it uses ",[49,3748,3749],{},"key = value"," assignments, ",[49,3752,3753],{},"[server]"," tables, and ",[49,3756,3757],{},"[[products]]"," arrays of tables. Strings are quoted, booleans are written only as ",[49,3760,3638],{},[49,3762,3566],{},", and dates and times have their own types. Version 1.0 consolidated these rules in the ",[38,3765,3768],{"href":3766,"rel":3767},"https:\u002F\u002Ftoml.io\u002Fen\u002Fv1.0.0",[429],"official TOML specification",[11,3770,3771,3772,3774],{},"That explicitness suits application and package settings that people edit regularly. It also narrows the format's scope. A TOML document always has a table at its root, no ",[49,3773,1741],{}," value exists, and a graph with shared nodes represented by YAML aliases has no direct equivalent. TOML is consequently neither repaired YAML nor newer JSON; it addresses a more focused configuration problem.",[15,3776,3778],{"id":3777},"the-data-path-should-determine-the-format","The data path should determine the format",[11,3780,3781],{},"For network exchange and machine-generated data, JSON is often the simplest contract because its model is small and compatible implementations are almost universal. YAML can be more comfortable when people frequently edit the configuration and need comments, multiline text, or reusable sections, provided the team controls the parser and agrees on a schema. TOML works well when the document naturally divides into named tables and explicit value types matter.",[11,3783,3784,3787,3788,3791,3792,3794],{},[49,3785,3786],{},".env"," files and shell ",[49,3789,3790],{},"export"," statements sit closer to the process boundary. An environment variable ultimately contains a string, so nesting, arrays, numbers, booleans, and ",[49,3793,1741],{}," must be encoded through conventions. There is no single representation that every dotenv library can reverse into the same structure. Conversion between these formats should therefore be understood as a transformation between data models, not a cosmetic exchange of braces for indentation.",[15,3796,3798],{"id":3797},"what-the-converter-actually-does","What the converter actually does",[11,3800,510,3801,3805,3806,3808,3809,3811],{},[38,3802,3804],{"href":3803},"\u002Ftools\u002Fyaml-json-converter","YAML, JSON, TOML & .env Converter"," works with five representations: JSON, YAML, TOML, ",[49,3807,3786],{},", and shell ",[49,3810,3790],{}," statements. It first parses the source into an ordinary JavaScript value and then serializes that value into the selected format. This intermediate layer makes common configuration files convertible in either direction, but it cannot retain features that do not exist in the JavaScript model, including comments, YAML aliases, or TOML's distinct date type.",[11,3813,3814,3815,1121,3818,3821,3822,1121,3824,3826,3827,673,3829,673,3832,1228,3834,3837],{},"JSON is handled by native ",[49,3816,3817],{},"JSON.parse",[49,3819,3820],{},"JSON.stringify",". For YAML and TOML, the tool implements practical syntax subsets rather than trying to replace complete standards-compliant parsers. Its YAML path understands ordinary mappings, sequences, and scalars, deliberately treats only ",[49,3823,3638],{},[49,3825,3566],{}," as booleans, and preserves ",[49,3828,3562],{},[49,3830,3831],{},"YES",[49,3833,3706],{},[49,3835,3836],{},"OFF"," as strings. Complex tags, anchors, merge keys, block scalars, and multiple documents require a specialized library. Two or four spaces are the safe indentation choices for YAML output because YAML does not permit tabs for indentation.",[11,3839,3840,3841,3843],{},"The TOML path supports tables, arrays of tables, strings, numbers, dates, and common compound values, but a date becomes a string after passing through JavaScript. TOML also has no ",[49,3842,1741],{},", so the converter leaves an explanatory comment or reports a replacement inside a compound value instead of hiding the loss. An array or scalar at the document root cannot be serialized as TOML either, since the root must be a table.",[11,3845,3846,3847,3849,3850,3853,3854,3856],{},"When targeting ",[49,3848,3786],{}," or shell, nested values are flattened into names such as ",[49,3851,3852],{},"DATABASE__HOST",", while array elements receive numeric path segments. The reverse conversion understands the same convention, but every leaf value remains a string. Source auto-detection is heuristic, particularly when a short input resembles both TOML and ",[49,3855,3786],{},", so users can pin the source format manually.",[11,3858,3859],{},"All conversion runs locally in the browser. The tool reports renamed keys and type losses, swaps the conversion direction with one control, and copies the result, but a production migration should still pass the output through the exact parser and schema used by the destination application.",[15,3861,358],{"id":357},[357,3863],{":slug":3864},"[\"json-toolbox\",\"xml-toolbox\",\"docker-toolbox\",\"systemd-builder\"]",[1779,3866,3867],{},"html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":363,"searchDepth":364,"depth":364,"links":3869},[3870,3871,3872,3873,3874,3875],{"id":3570,"depth":364,"text":3571},{"id":3599,"depth":364,"text":3600},{"id":3736,"depth":364,"text":3737},{"id":3777,"depth":364,"text":3778},{"id":3797,"depth":364,"text":3798},{"id":357,"depth":364,"text":358},"2026-08-20","How JSON, YAML, and TOML emerged, where the Norway Problem came from, and what gets lost when configuration moves between formats.",[3879,3881,3884,3887,3890,3893,3896],{"q":1794,"a":3880},"JSON grew out of JavaScript object notation, and several developers independently discovered similar ways to exchange data. Douglas Crockford recalled arriving at it at State Software in April 2001, then registering json.org and documenting the format in 2002. The first IETF specification was RFC 4627 in 2006; the current Internet Standard, RFC 8259, was published in 2017.",{"q":3882,"a":3883},"What is the Norway Problem in YAML?","Under the YAML 1.1 type schema, unquoted NO, YES, ON, and OFF were resolved as booleans. A field such as country: NO could therefore become false. YAML 1.2 Core Schema recognizes variants of true and false as booleans, but actual behavior still depends on the parser's version and selected schema.",{"q":3885,"a":3886},"How do JSON, YAML, and TOML differ?","JSON is a compact interchange format with a small set of data types. YAML offers human-friendly notation, comments, multiline values, anchors, and other features, but resolution depends on the selected schema. TOML is aimed primarily at configuration and maps unambiguously to tables containing typed values.",{"q":3888,"a":3889},"Can TOML represent every JSON or YAML document?","No. A TOML document always has a table at its root and has no null value, although it does have distinct date and time types. YAML can also describe graphs through anchors and aliases, and its data model is wider than a plain JavaScript object. Some documents therefore cannot move into TOML or JSON without losing a type, structure, or meaning.",{"q":3891,"a":3892},"Why can two programs interpret the same YAML differently?","The YAML version and selected schema affect how unquoted scalars are resolved, while libraries do not always use the same defaults. Portable configuration should quote ambiguous strings and pin the project's parser version and settings.",{"q":3894,"a":3895},"Can JSON or YAML be converted to .env and back without loss?","Not always. Environment variables are strings and do not preserve types, nesting, or null. This converter flattens objects into double-underscore keys and reconstructs them on the way back, but that is the tool's convention rather than a universal dotenv standard.",{"q":3897,"a":3898},"Does the converter support every YAML and TOML feature?","No. It targets common configuration files and implements practical subsets of both formats. Complex YAML with tags, anchors, merge keys, or multiple documents, as well as uncommon TOML constructs, should be checked with the parser and validator used by the destination project.","\u002Fimages\u002Fblog\u002Fblog_yaml-json-converter.webp",{},"\u002Fblog\u002Fen\u002Fyaml-json-converter",{"title":3550,"description":3877},"blog\u002Fen\u002Fyaml-json-converter",[3905,3906,3907,3908],"YAML","JSON","TOML","Config formats","ILhU-DrCSFBqp4nFzCw3Hk6KyO4_y1mq3go-nxXoljE",{"id":3911,"title":3912,"body":3913,"date":4368,"description":4369,"extension":376,"faq":4370,"image":4386,"lang":394,"meta":4387,"navigation":396,"path":4388,"published":396,"readTime":4389,"seo":4390,"stem":4391,"tags":4392,"updated":405,"__hash__":4396},"blog\u002Fblog\u002Fen\u002Finvisible-marks-in-ai-images.md","The Three Invisible Marks on an AI Image",{"type":8,"value":3914,"toc":4359},[3915,3918,3922,3937,3956,3962,3966,3969,3984,3987,3990,3994,3997,4000,4027,4030,4034,4037,4195,4199,4278,4281,4284,4288,4296,4354,4356],[11,3916,3917],{},"An image generated today can leave with evidence at three separate levels, made by different mechanisms with different failure modes. They get discussed as though they were one thing — \"the AI watermark\" — and they are not. One may be a text field. One may be a signed document. Another may be in the picture itself.",[15,3919,3921],{"id":3920},"layer-one-metadata-which-is-a-claim","Layer one: metadata, which is a claim",[11,3923,3924,3925,3928,3929,3932,3933,3936],{},"The simplest marker is a string in the file header. ",[49,3926,3927],{},"xmp:CreatorTool"," saying ",[49,3930,3931],{},"DALL-E 3",". A PNG text chunk called ",[49,3934,3935],{},"parameters"," holding the full prompt, seed, sampler and model name, which is what Stable Diffusion interfaces write by default and what makes local generations the easiest of all to identify.",[11,3938,3939,3940,3943,3944,3947,3948,3951,3952,3955],{},"The one worth knowing by name is the IPTC digital source type. It is an XMP property, ",[49,3941,3942],{},"Iptc4xmpExt:DigitalSourceType",", and its value is an IRI ending in a token from a controlled vocabulary: ",[49,3945,3946],{},"trainedAlgorithmicMedia"," for something a model generated, ",[49,3949,3950],{},"compositeWithTrainedAlgorithmicMedia"," for a composite containing generated elements, ",[49,3953,3954],{},"digitalCapture"," for a photograph taken by a camera. It is the closest thing to a standard machine-readable \"this is AI\" flag, and a growing number of generators and camera makers write it.",[11,3957,3958,3959,3961],{},"It is also a text field. Anyone with a metadata editor can add ",[49,3960,3946],{}," to a photograph or delete it from a generated image, in seconds, without touching a pixel. Metadata tells you what a file says about itself. That is genuinely useful and it is not evidence.",[15,3963,3965],{"id":3964},"layer-two-c2pa-which-is-a-signed-document","Layer two: C2PA, which is a signed document",[11,3967,3968],{},"Content Credentials solve the \"anyone can edit it\" problem the way the web solves it everywhere else: a signature. A C2PA manifest is a structured record — a claim naming the generator, a set of assertions describing what was done, thumbnails, hashes of the asset itself — wrapped in a COSE signature with an X.509 certificate chain.",[11,3970,3971,3972,3975,3976,3979,3980,3983],{},"It is stored as JUMBF, the JPEG box format, and where that box sits depends on the container. In a JPEG it goes into APP11 marker segments, each prefixed with the two bytes ",[49,3973,3974],{},"JP",", a box instance number and a packet sequence number, because a manifest with a thumbnail comfortably exceeds the 64 KB a single segment can hold and has to be split across several. In a PNG it goes into a ",[49,3977,3978],{},"caBX"," chunk. In a WebP it goes into a RIFF chunk with the fourcc ",[49,3981,3982],{},"C2PA",". Plenty of tools that claim C2PA support look only in APP1 and APP2, find nothing, and report the file as clean.",[11,3985,3986],{},"Reading a manifest and verifying one are different jobs. Reading gives you the claim: the generator, the actions, the declared source type, the names on the certificate. Verifying means checking the signature against a trust list and confirming that the hashes still match the pixels — which is what catches a manifest copied out of one file and pasted into another. Anything that only reads, including a browser tool, is reporting what the file asserts.",[11,3988,3989],{},"And the manifest is fragile by construction. It is bound to the bytes of the asset; edit the asset with something that does not understand C2PA and the binding is broken, so the sane behaviour is to drop it. Open in an editor, export, upload to a platform that re-encodes: gone. Not defeated — simply absent, indistinguishable from an image that never had one.",[15,3991,3993],{"id":3992},"layer-three-the-pixels-which-do-not-wash-out","Layer three: the pixels, which do not wash out",[11,3995,3996],{},"SynthID takes the opposite approach. Instead of attaching a record to the file, it perturbs the pixel values themselves, in a pattern a matching detector can recognise and an eye cannot see. Because the mark is in the picture, it is designed to remain detectable after transformations that often remove the other two: re-encoding, cropping, colour adjustment, filters and screenshots.",[11,3998,3999],{},"What you can run is classical steganalysis and signal forensics, which examine several statistical and physical characteristics:",[4001,4002,4003,4009,4015,4021],"ol",{},[301,4004,4005,4008],{},[26,4006,4007],{},"Low-bit planes and RS steganalysis."," Do the low-order bits look written to? Flip the least significant bit of a natural image and it gets locally rougher in a predictable way; flip bits that already carry a payload and it does not, and the gap between the two behaviours yields an estimated share of pixels carrying an embedded bit. Per-block RS estimates generate an interactive heatmap of localized anomalies.",[301,4010,4011,4014],{},[26,4012,4013],{},"Frequency domain and 2D FFT."," Mathematical 2D Fourier spectra of generative imagery often display artificial frequency harmonics: transposed convolution and upscaling layers leave characteristic periodic grids (\"deconvolution artifacts\"). Here, it is vital to mathematically isolate standard JPEG $8 \\times 8$ block compression harmonics from anomalous generative lattice peaks, and measure the radial power spectral density slope $\\alpha$ (optical camera photographs decay with power-law distribution $1\u002Ff^\\alpha$ where $\\alpha \\approx 1.8–2.2$).",[301,4016,4017,4020],{},[26,4018,4019],{},"DCT Benford's Law conformance."," The distribution of the first significant digit of AC coefficients in $8 \\times 8$ Discrete Cosine Transform blocks in unmanipulated photographs follows Generalized Benford's Law. Generative synthesis and tampering induce statistical divergence measured via the Kolmogorov-Smirnov distance ($D_$).",[301,4022,4023,4026],{},[26,4024,4025],{},"Sensor noise residual profiling."," In physical camera captures, photon shot noise varies with luminance (higher relative variance in shadows). Generative models with additive Gaussian noise or aggressive smoothing often exhibit unnaturally homogeneous noise variance across all brightness bands.",[11,4028,4029],{},"Be clear about what these estimates are worth. Heavy JPEG recompression, aggressive denoising and ordinary steganography all move the numbers. They answer \"what statistical and spectral anomalies exist in these pixels\", never \"which commercial watermark is this\".",[15,4031,4033],{"id":4032},"how-to-interpret-pixel-analysis-metrics","How to interpret pixel analysis metrics",[11,4035,4036],{},"When container markers are absent or stripped, forensic analysis relies on spectral and statistical anomalies. Here is how to interpret key indicators:",[75,4038,4039,4055],{},[78,4040,4041],{},[81,4042,4043,4046,4049,4052],{},[84,4044,4045],{},"Metric",[84,4047,4048],{},"Natural photo baseline",[84,4050,4051],{},"AI generation \u002F manipulation indicator",[84,4053,4054],{},"Physical meaning",[91,4056,4057,4073,4098,4123,4149,4171],{},[81,4058,4059,4064,4067,4070],{},[96,4060,4061],{},[26,4062,4063],{},"2D FFT Spectrogram",[96,4065,4066],{},"Smooth concentric decay from center to corners",[96,4068,4069],{},"Bright isolated constellation dots or sharp rays",[96,4071,4072],{},"Visualizes frequency energy distribution: center = shapes & low frequencies, edges = fine details & noise. Dots far from the center reveal periodic generator upsampling lattices.",[81,4074,4075,4080,4085,4095],{},[96,4076,4077],{},[26,4078,4079],{},"Radial PSD slope (α)",[96,4081,4082],{},[26,4083,4084],{},"1.80 – 2.20",[96,4086,4087,4090,4091,4094],{},[26,4088,4089],{},"> 2.60"," (plastic smoothness) or ",[26,4092,4093],{},"\u003C 1.50"," (flat noise floor)",[96,4096,4097],{},"Measures power-law decay rate $1\u002Ff^\\alpha$. In camera optics, physical light creates a steady slope near 2.0. Values above 2.6 reflect over-smoothed, \"plastic\" synthetic textures.",[81,4099,4100,4105,4114,4120],{},[96,4101,4102],{},[26,4103,4104],{},"Deconvolution grid",[96,4106,4107,4110,4111,219],{},[49,4108,4109],{},"0 non-JPEG peaks"," (",[49,4112,4113],{},"clean",[96,4115,4116,4119],{},[49,4117,4118],{},"grid anomaly"," (non-JPEG peaks detected)",[96,4121,4122],{},"Detects periodic upscaling harmonics (transposed convolution). JPEG $8 \\times 8$ block compression harmonics are filtered out, isolating genuine neural deconvolution lattices.",[81,4124,4125,4130,4138,4146],{},[96,4126,4127],{},[26,4128,4129],{},"DCT Benford's Law",[96,4131,4132,4110,4135,219],{},[26,4133,4134],{},"KS \u003C 0.085",[49,4136,4137],{},"compliant",[96,4139,4140,4110,4143,219],{},[26,4141,4142],{},"KS > 0.090",[49,4144,4145],{},"deviation",[96,4147,4148],{},"In authentic photos, leading digits of $8 \\times 8$ DCT AC coefficients follow $P(d)=\\log_{10}(1+1\u002Fd)$. Mathematical pixel synthesis by AI models induces statistical divergence.",[81,4150,4151,4156,4159,4168],{},[96,4152,4153],{},[26,4154,4155],{},"Noise homogeneity",[96,4157,4158],{},"Low (natural variance spread)",[96,4160,4161,4164,4165,219],{},[26,4162,4163],{},"> 78%"," with high total variance (",[49,4166,4167],{},"synthetic pattern",[96,4169,4170],{},"In physical sensors, photon shot noise is inherently stronger in shadows. Generative additive noise often produces unnaturally uniform variance across all brightness bands.",[81,4172,4173,4178,4184,4192],{},[96,4174,4175],{},[26,4176,4177],{},"RS steganalysis",[96,4179,4180,4183],{},[26,4181,4182],{},"1% – 5%"," (quantisation noise baseline)",[96,4185,4186,4110,4189,219],{},[26,4187,4188],{},"> 12%",[49,4190,4191],{},"anomaly",[96,4193,4194],{},"Estimates the share of pixels with altered low-order bits by measuring roughness shifts in regular and singular groups under LSB bit flipping.",[15,4196,4198],{"id":4197},"what-survives-what","What survives what",[75,4200,4201,4215],{},[78,4202,4203],{},[81,4204,4205,4207,4210,4212],{},[84,4206],{},[84,4208,4209],{},"Metadata",[84,4211,3982],{},[84,4213,4214],{},"Pixel watermark",[91,4216,4217,4230,4241,4253,4264],{},[81,4218,4219,4222,4225,4227],{},[96,4220,4221],{},"Re-save in an editor",[96,4223,4224],{},"usually removed",[96,4226,4224],{},[96,4228,4229],{},"may remain detectable",[81,4231,4232,4235,4237,4239],{},[96,4233,4234],{},"Crop or resize",[96,4236,4224],{},[96,4238,4224],{},[96,4240,4229],{},[81,4242,4243,4246,4249,4251],{},[96,4244,4245],{},"Screenshot",[96,4247,4248],{},"removed",[96,4250,4248],{},[96,4252,4229],{},[81,4254,4255,4258,4260,4262],{},[96,4256,4257],{},"Upload to most platforms",[96,4259,4224],{},[96,4261,4224],{},[96,4263,4229],{},[81,4265,4266,4269,4272,4275],{},[96,4267,4268],{},"Edit in a text editor",[96,4270,4271],{},"trivially forged",[96,4273,4274],{},"breaks the signature",[96,4276,4277],{},"untouched",[11,4279,4280],{},"Read down the columns and the practical situation is plain. The two layers that carry a name and a signature are fragile in ordinary workflows, while a proprietary pixel mark is designed to be more resilient and needs its matching detector. Which is why the absence of readable markers means much less than people assume: after a re-encode, missing container evidence is common.",[11,4282,4283],{},"The regulation arriving in parallel does not resolve this. Article 50 of the EU AI Act requires providers to mark synthetic output in a machine-readable form, and those obligations started applying in August 2026 — but it does not mandate a single technical format, which is precisely why you find manifests, source-type fields and invisible pixel patterns in circulation at once.",[15,4285,4287],{"id":4286},"what-the-browser-tool-can-do","What the browser tool can do",[11,4289,4290,4291,4295],{},"I built ",[38,4292,4294],{"href":4293},"\u002Ftools\u002Fai-watermark-detector","an AI watermark detector"," that inspects supported evidence in the browser without uploading the image to a server:",[298,4297,4298,4311,4323,4342,4348],{},[301,4299,4300,4303,4304,4307,4308,4310],{},[26,4301,4302],{},"Container & metadata inspection:"," scans EXIF fields, PNG text chunks, XMP properties, and IPTC ",[49,4305,4306],{},"DigitalSourceType"," vocabularies (including ",[49,4309,3946],{}," and generator parameters).",[301,4312,4313,4316,4317,4319,4320,4322],{},[26,4314,4315],{},"C2PA \u002F Content Credentials parsing:"," locates and reassembles fragmented JUMBF boxes across JPEG APP11 segments, PNG ",[49,4318,3978],{}," chunks, and WebP ",[49,4321,3982],{}," chunks, decoding the CBOR manifest and claim assertions (stating plainly that cryptographic signature validation is not performed in the browser).",[301,4324,4325,4328],{},[26,4326,4327],{},"Pixel & spectral signal forensics in a Web Worker:",[298,4329,4330,4333,4336,4339],{},[301,4331,4332],{},"Per-channel RS steganalysis with estimated embedding rate and interactive per-block heatmap;",[301,4334,4335],{},"2D FFT spectrum analyzer with thermal power spectrogram visualization, radial slope $\\alpha$ decay estimation, and isolation of JPEG $8 \\times 8$ harmonics from non-JPEG deconvolution grid spikes;",[301,4337,4338],{},"$8 \\times 8$ block DCT Generalized Benford's Law compliance testing;",[301,4340,4341],{},"Sensor residual noise variance homogeneity evaluation across 5 luminance bands.",[301,4343,4344,4347],{},[26,4345,4346],{},"Verified re-encoding:"," generates cleaned JPEG, PNG, or WebP output, re-scanning the container before download and blocking the save if removable EXIF, XMP, IPTC, or C2PA evidence remains. Optional low-bit perturbation is experimental and does not guarantee removal of proprietary pixel watermarks.",[301,4349,4350,4353],{},[26,4351,4352],{},"Client privacy & export:"," all computations run strictly on the client; supports direct cross-origin image fetching without a server proxy and full JSON diagnostic report exports.",[15,4355,358],{"id":357},[357,4357],{":slug":4358},"[\"ai-watermark-detector\",\"exif-inspector\",\"lsb-steganography\",\"image-optimizer\"]",{"title":363,"searchDepth":364,"depth":364,"links":4360},[4361,4362,4363,4364,4365,4366,4367],{"id":3920,"depth":364,"text":3921},{"id":3964,"depth":364,"text":3965},{"id":3992,"depth":364,"text":3993},{"id":4032,"depth":364,"text":4033},{"id":4197,"depth":364,"text":4198},{"id":4286,"depth":364,"text":4287},{"id":357,"depth":364,"text":358},"2026-08-19","A signed manifest in APP11, editable XMP fields, and proprietary pixel watermarks. Their evidence and failure modes are not the same.",[4371,4374,4377,4380,4383],{"q":4372,"a":4373},"How can I tell if an image was generated by AI?","Nothing tells you with certainty. Three things can tell you something: metadata fields naming the generator, a C2PA manifest attributed to the tool that made the file, and statistical traces in the pixels. The first two are commonly dropped by a re-save, and the third only produces a probability. A file with no readable markers is not proof of human origin; it only means the supported checks found no surviving evidence.",{"q":4375,"a":4376},"What is C2PA and where is it stored in a file?","C2PA — Content Credentials — is a signed manifest describing where a file came from: which tool made it, which edits followed, and a certificate chain over the lot. It is stored as a JUMBF box, which lives in APP11 marker segments in a JPEG, in a caBX chunk in a PNG, and in a chunk with the fourcc C2PA in a WebP. Larger manifests are split across several APP11 packets and have to be reassembled before anything can be read.",{"q":4378,"a":4379},"Does re-saving an image remove the AI watermark?","A conventional re-save usually drops metadata and the C2PA manifest because both live in file containers rather than in the picture. It does not reliably remove a pixel-domain watermark like SynthID, which is designed to remain detectable after recompression, cropping, colour adjustment and screenshots. Re-encoding can improve privacy from EXIF and GPS, but it does not guarantee anonymity from a watermark.",{"q":4381,"a":4382},"Can SynthID be detected without Google's tools?","Not identified as SynthID specifically — the detector is Google's and the key is not public. What is available is classical steganalysis: RS analysis estimates what share of pixels carry an embedded bit, and a per-block version shows where. It answers whether the low bits look written to, not who wrote them.",{"q":4384,"a":4385},"Does the EU AI Act require AI images to be marked?","Article 50 makes providers of generative systems mark synthetic output in a machine-readable way, and those transparency obligations started applying in August 2026. It says the mark has to be there and be detectable; it does not mandate one technical format, which is why you meet C2PA manifests, IPTC source-type fields and invisible pixel watermarks all at once, and why an image can carry all three or none.","\u002Fimages\u002Fblog\u002Fblog_invisible-marks-in-ai-images.webp",{},"\u002Fblog\u002Fen\u002Finvisible-marks-in-ai-images",6,{"title":3912,"description":4369},"blog\u002Fen\u002Finvisible-marks-in-ai-images",[4393,3982,4394,4395],"AI watermarks","Image provenance","Steganography","RWdR17iCUalqbjHYq7hOdtcY-Ar5NFv_7Yg1BI06mjU",{"id":4398,"title":4399,"body":4400,"date":4618,"description":4619,"extension":376,"faq":4620,"image":4642,"lang":394,"meta":4643,"navigation":396,"path":4644,"published":396,"readTime":4389,"seo":4645,"stem":4646,"tags":4647,"updated":405,"__hash__":4657},"blog\u002Fblog\u002Fen\u002Fencryption-toolbox.md","From Hand Ciphers to AES: How the Security Model Changed",{"type":8,"value":4401,"toc":4609},[4402,4405,4409,4418,4421,4430,4441,4445,4458,4468,4485,4488,4492,4495,4504,4507,4511,4514,4527,4531,4540,4562,4571,4577,4581,4588,4591,4594,4601,4604,4606],[11,4403,4404],{},"Atbash, Caesar's cipher, Enigma, and AES are often placed on one timeline as if each method simply added more complexity than the last. The task itself changed along the way. A hand cipher was meant to delay someone who intercepted a letter; a machine cipher faced organized signals intelligence, while a modern algorithm must survive public analysis, automated attack, and accidental modification in transit. The history of cryptography is therefore also a history of expanding threat models.",[15,4406,4408],{"id":4407},"fixed-substitution-hid-text-without-creating-a-large-secret","Fixed substitution hid text without creating a large secret",[11,4410,4411,4412,4417],{},"The Masoretic text of Jeremiah contains the name Sheshach, traditionally connected to Babel through a reversal of the Hebrew alphabet. The first letter exchanges with the last, the second with the penultimate, and the method's name comes from the first pairs: Aleph-Tav-Bet-Shin. An ",[38,4413,4416],{"href":4414,"rel":4415},"https:\u002F\u002Fwww.biblegateway.com\u002Fresources\u002Fencyclopedia-of-the-bible\u002FAtbash",[429],"Encyclopedia of the Bible entry"," identifies the device as Atbash while also noting that its purpose in the passage is unclear. Babylon is named openly in the same chapter, so describing Atbash as a documented secret channel for ancient scribes goes beyond the evidence.",[11,4419,4420],{},"Atbash has no selectable key because its mapping is always the same. It can puzzle an unprepared reader, but anyone who knows the rule recovers the entire message immediately. By modern standards, that makes it a fixed transformation rather than meaningful confidentiality.",[11,4422,4423,4424,4429],{},"The evidence for Caesar's cipher comes from another later source. In chapter 56 of his biography, ",[38,4425,4428],{"href":4426,"rel":4427},"https:\u002F\u002Fpenelope.uchicago.edu\u002FThayer\u002FE\u002FRoman\u002FTexts\u002FSuetonius\u002F12Caesars\u002FJulius%2A.html#56",[429],"Suetonius wrote"," that Julius Caesar changed the order of letters in confidential correspondence so that a reader should substitute D for A. That describes a shift of three positions. Suetonius was writing in the second century CE, long after Caesar's life, but his account attached Caesar's name to an entire family of shift ciphers.",[11,4431,4432,4433,4436,4437,4440],{},"Here the secret is the shift value: ",[49,4434,4435],{},"E(x) = (x + n) mod m",", where ",[49,4438,4439],{},"m"," is the alphabet size. The key space is small enough to enumerate completely. More importantly, the same plaintext letter always becomes the same ciphertext letter, preserving the language's frequency pattern. Merely adding more symbols would not fix that structural weakness; the next important step was to change the mapping as the message progressed.",[15,4442,4444],{"id":4443},"a-repeating-key-obscured-frequencies-but-left-a-period","A repeating key obscured frequencies but left a period",[11,4446,4447,4448,4451,4452,4457],{},"The system commonly called the Vigenère cipher uses a keyword whose letters select a sequence of different shifts. One plaintext letter can consequently have several ciphertext forms, so a single frequency table no longer gives a direct solution. The familiar attribution, however, hides a complicated history. Giovan Battista Bellaso published the repeating-letter key in ",[1321,4449,4450],{},"La cifra del Sig. Giovan Battista Bellaso"," in 1553, whereas Blaise de Vigenère's 1586 treatise particularly developed an autokey principle. An ",[38,4453,4456],{"href":4454,"rel":4455},"https:\u002F\u002Fwww.nsa.gov\u002Fportals\u002F75\u002Fdocuments\u002Fabout\u002Fcryptologic-heritage\u002Fhistorical-figures-publications\u002Fpublications\u002Fcryptologic-quarterly\u002Fcryptologic-quarterly-2014-01.pdf",[429],"NSA survey of Renaissance cryptography"," confirms that distinction, making it inaccurate to present the modern scheme as Vigenère's personal invention.",[11,4459,4460,4461,507],{},"Repeating a key introduced another weakness. When the same plaintext fragment falls under the same part of the key, corresponding groups can recur in the ciphertext. Distances between those repetitions help estimate the period; the text can then be divided into several sequences, each of which once again resembles an ordinary shift cipher. Friedrich Kasiski publicly described such an approach in his 1863 book ",[38,4462,4465],{"href":4463,"rel":4464},"https:\u002F\u002Fbooks.google.de\u002Fbooks?hl=de&id=I1PgeY9uJ08C",[429],[1321,4466,4467],{},"Die Geheimschriften und die Dechiffrir-Kunst",[11,4469,4470,4471,4474,4475,4478,4479,4484],{},"In 1920, William Friedman published the index of coincidence, a statistic that measures the chance of drawing two identical letters from a sample. A uniform alphabet of ",[49,4472,4473],{},"c"," symbols has a baseline near ",[49,4476,4477],{},"1\u002Fc","; natural language differs because its letters are unevenly distributed. A simple substitution only renames characters and preserves their counts, while a polyalphabetic cipher alters the observed distribution. Splitting the text by a candidate period can expose language-like structure again when the proposed key length is correct. The ",[38,4480,4483],{"href":4481,"rel":4482},"https:\u002F\u002Fwww.nsa.gov\u002FHistory\u002FCryptologic-History\u002FHistorical-Events\u002FHistorical-Events-List\u002F?Page=2",[429],"NSA historical timeline"," dates Friedman's first publication to 1920.",[11,4486,4487],{},"None of these measurements identifies a cipher with certainty. Results vary with language, alphabet, and sample length, and repetitions also occur by chance. They work as several pieces of evidence, not as an automatic proof.",[15,4489,4491],{"id":4490},"enigma-mechanized-the-changing-alphabet","Enigma mechanized the changing alphabet",[11,4493,4494],{},"The Enigma rotor machine performed at mechanical speed what was cumbersome by hand. After every keypress, the electrical route through its rotors changed, so the next letter used another substitution. Arthur Scherbius developed an early machine in 1918, and commercial and military versions grew more elaborate over time. By the start of the Second World War, the secret included rotor choice, order and starting positions, plugboard connections, and settings that changed regularly.",[11,4496,4497,4498,4503],{},"The familiar account in which Alan Turing broke Enigma omits the necessary first half of the story. Marian Rejewski made the first decisive breakthrough at the Polish Cipher Bureau at the end of 1932, then developed changing-key recovery techniques with Jerzy Różycki and Henryk Zygalski. Poland gave its methods, Zygalski sheets, cryptologic bomb, and machine copies to British and French representatives in July 1939. The ",[38,4499,4502],{"href":4500,"rel":4501},"https:\u002F\u002Faw.gov.pl\u002Fen\u002Fhistory\u002Fenigma-decryption\u002F183,Enigma-decryption.html",[429],"Polish Foreign Intelligence Agency's history"," documents that sequence; at Bletchley Park, Turing, Gordon Welchman, and a much larger organization extended the foundation to new machine variants and wartime interception at scale.",[11,4505,4506],{},"Their success was not simply a case of finding enough computing power. Cryptanalysis combined a mathematical model of the machine, intelligence material, predictable message fragments, procedural mistakes, and special-purpose search devices. Enigma demonstrated that a sophisticated mechanism must be evaluated together with the operating system around it.",[15,4508,4510],{"id":4509},"des-turned-key-length-into-a-publicly-testable-limit","DES turned key length into a publicly testable limit",[11,4512,4513],{},"Once computers became widespread, a cipher had to process bits, have an exact public specification, and produce compatible implementations. In 1977, the US National Bureau of Standards adopted DES as a federal standard. It transformed 64-bit blocks through 16 rounds and used 56 independent key bits; the other 8 bits in its 64-bit key representation served as parity checks.",[11,4515,4516,4517,4522,4523,4526],{},"DES resisted the best-known attacks on its round construction for years, but its key space remained finite and measurable. In July 1998, the ",[38,4518,4521],{"href":4519,"rel":4520},"https:\u002F\u002Fw2.eff.org\u002FPrivacy\u002FCrypto\u002FCrypto_misc\u002FDESCracker\u002F",[429],"EFF DES Cracker"," recovered a contest key in 56 hours. In January 1999, the same machine working with distributed.net reduced the time to 22 hours and 15 minutes. The demonstration did not show that the DES round function had suddenly failed. It showed that specialized hardware could afford to search ",[49,4524,4525],{},"2^56"," candidates.",[15,4528,4530],{"id":4529},"aes-was-selected-openly-but-aes-alone-is-not-a-complete-scheme","AES was selected openly, but AES alone is not a complete scheme",[11,4532,4533,4534,4539],{},"NIST began an open process to replace DES in 1997. Researchers analyzed 15 accepted algorithms, then five finalists, before NIST selected Rijndael by Joan Daemen and Vincent Rijmen in October 2000. The design became ",[38,4535,4538],{"href":4536,"rel":4537},"https:\u002F\u002Fcsrc.nist.gov\u002Fpubs\u002Ffips\u002F197\u002Ffinal",[429],"FIPS 197"," in 2001. The public competition was central to the result: confidence came from a published specification, independent scrutiny, implementation performance, and interoperability rather than secrecy about the algorithm.",[11,4541,4542,4543,4546,4547,673,4550,673,4553,1228,4556,4558,4559,4561],{},"AES always processes a 128-bit block and accepts 128, 192, or 256-bit keys, using 10, 12, or 14 rounds respectively. With AES-256, an initial ",[49,4544,4545],{},"AddRoundKey"," is followed by 13 rounds containing ",[49,4548,4549],{},"SubBytes",[49,4551,4552],{},"ShiftRows",[49,4554,4555],{},"MixColumns",[49,4557,4545],{},"; the fourteenth omits ",[49,4560,4555],{},". This exception matters because the common summary that every round contains the same four operations is technically wrong.",[11,4563,4564,4565,4570],{},"A block cipher alone still does not explain how to protect an arbitrary-length message. A system also needs a mode of operation, nonce rules, and an integrity check. ",[38,4566,4569],{"href":4567,"rel":4568},"https:\u002F\u002Fcsrc.nist.gov\u002Fpubs\u002Fsp\u002F800\u002F38\u002Fd\u002Ffinal",[429],"GCM in NIST SP 800-38D"," is an authenticated-encryption mode: it conceals data and produces an authentication tag, causing modified ciphertext or the wrong key to fail verification. Its critical condition is that a nonce must not repeat under the same key. A sound AES primitive does not make a complete system sound when its mode, parameters, or key management are wrong.",[11,4572,4573],{},[1068,4574],{"alt":4575,"src":4576},"Five diagrams trace the change from Caesar substitution and a repeating key to Enigma rotors, the DES network, and authenticated AES-GCM","\u002Fimages\u002Fblog\u002Finline_encryption-toolbox.webp",[15,4578,4580],{"id":4579},"what-encryption-toolbox-demonstrates","What Encryption Toolbox demonstrates",[11,4582,510,4583,4587],{},[38,4584,4586],{"href":4585},"\u002Ftools\u002Fencryption-toolbox","Encryption Toolbox"," brings four different activities together without making them equivalent. Cipher Identifier first looks for structural signatures of Morse, Base64, hexadecimal, JWT, ADFGX, and other formats, then uses coincidence, frequency, repetition, and candidate readability scores. Its output is a heuristic ranking. The displayed percentage is not a calibrated statistical probability, and a short or multiply transformed string can easily produce the wrong leader.",[11,4589,4590],{},"Multi Encoder runs a message through the available encodings, symbolic alphabets, classical ciphers, and one-way transformations. It decodes reversible results to mark an exact round trip, while a separate state identifies expected loss of case, spacing, or punctuation. Base64, hexadecimal, Morse, and hashes do not become encryption merely because the interface places them beside ciphers.",[11,4592,4593],{},"Cipher Workbench is designed for learning classical systems. It lets the reader change keys and alphabets, compare the Vigenère family, Playfair, Polybius, Bifid, ADFGX, transpositions, and simple substitutions, and enumerate candidates for selected methods. Those operations are useful for education and puzzles, but they do not provide modern confidentiality.",[11,4595,4596,4597,4600],{},"The AES tab uses the browser's Web Crypto API. It derives a 256-bit key from a password with PBKDF2-SHA256, 200,000 iterations, and a random 16-byte salt. AES-GCM receives a random 12-byte IV, while Web Crypto supplies a 128-bit authentication tag by default. The tool packs the result as ",[49,4598,4599],{},"salt || IV || ciphertext+tag"," and Base64-encodes it. Its code contains no request that sends the entered text or password to a server.",[11,4602,4603],{},"That format has practical limits. A weak password remains weak, PBKDF2 is not memory-hard, the work factor is fixed, and the output carries no format version or parameters for future migration. The tool also exposes no associated-data field and is built for text. It should therefore be treated as a local utility for messages shared with the same convention, not as a replacement for an audited protocol, a secret manager, or dedicated file-encryption software.",[15,4605,358],{"id":357},[357,4607],{":slug":4608},"[\"encryption-toolbox\",\"universal-decoder\",\"hash-generator\",\"key-pair-generator\",\"shamir-secret\",\"visual-cryptography\"]",{"title":363,"searchDepth":364,"depth":364,"links":4610},[4611,4612,4613,4614,4615,4616,4617],{"id":4407,"depth":364,"text":4408},{"id":4443,"depth":364,"text":4444},{"id":4490,"depth":364,"text":4491},{"id":4509,"depth":364,"text":4510},{"id":4529,"depth":364,"text":4530},{"id":4579,"depth":364,"text":4580},{"id":357,"depth":364,"text":358},"2026-08-18","How cryptography moved from fixed letter substitutions to openly analyzed computer standards, why no single machine broke Enigma, and what AES-GCM protects.",[4621,4624,4627,4630,4633,4636,4639],{"q":4622,"a":4623},"Is Atbash the oldest known cipher?","Atbash is one of the oldest known forms of systematic letter substitution. Sheshach in the Book of Jeremiah has traditionally been read as Babel produced by reversing the Hebrew alphabet, but this does not document a complete system of secret correspondence or explain why it was used. It is safer to call it an ancient cryptographic substitution than the undisputed first cipher.",{"q":4625,"a":4626},"What is the source for Caesar's cipher?","Writing in the second century CE, Suetonius said that Julius Caesar replaced each letter in confidential correspondence with the fourth letter following it, which amounts to a shift of three positions. This is later testimony rather than a surviving instruction written by Caesar, so the distinction matters.",{"q":4628,"a":4629},"Who actually invented the Vigenère cipher?","Giovan Battista Bellaso described the repeating-keyword system now commonly called the Vigenère cipher in 1553. Blaise de Vigenère's 1586 treatise developed other polyalphabetic systems, especially an autokey principle. The familiar name became attached later and is historically misleading.",{"q":4631,"a":4632},"Who broke Enigma?","Marian Rejewski achieved the first decisive mathematical breakthrough at the Polish Cipher Bureau in 1932 and then worked with Jerzy Różycki and Henryk Zygalski. Poland transferred its methods and machine copies to British and French specialists in July 1939. A large Bletchley Park operation, including Alan Turing and Gordon Welchman, extended that foundation during the war.",{"q":4634,"a":4635},"Why was DES replaced by AES?","DES has an effective 56-bit key, and by the late 1990s specialized hardware had demonstrated an affordable exhaustive search of that space. NIST held an open competition for a new 128-bit block cipher supporting 128, 192, and 256-bit keys; the winning Rijndael design became AES in 2001.",{"q":4637,"a":4638},"How is AES different from AES-GCM?","AES transforms individual 128-bit blocks but does not by itself define how to protect a long message. GCM is a mode of operation that uses AES for confidentiality and produces an authentication tag for integrity. GCM security requires that a nonce never be repeated with the same key.",{"q":4640,"a":4641},"Can Cipher Identifier determine a cipher with certainty?","No. It recognizes characteristic alphabets and formats, then ranks candidates using statistical signals. Short samples, custom alphabets, several transformations in sequence, or random data can all produce ambiguous results, so its displayed percentage is a heuristic score rather than a probability.","\u002Fimages\u002Fblog\u002Fblog_encryption-toolbox.webp",{},"\u002Fblog\u002Fen\u002Fencryption-toolbox",{"title":4399,"description":4619},"blog\u002Fen\u002Fencryption-toolbox",[4648,4649,4650,4651,4652,4653,4654,4655,4656],"history of encryption","Caesar cipher","Atbash cipher","Vigenère cipher","Enigma","DES","AES-256-GCM","Rijndael","index of coincidence","478nfO4Y2mGaCLd9RyQ6_7Bhw_Y2UktiFHDBGnDodpo",{"id":4659,"title":4660,"body":4661,"date":4973,"description":4974,"extension":376,"faq":4975,"image":4991,"lang":394,"meta":4992,"navigation":396,"path":4993,"published":396,"readTime":4389,"seo":4994,"stem":4995,"tags":4996,"updated":405,"__hash__":5003},"blog\u002Fblog\u002Fen\u002Fhash-generator.md","MD5 Was Broken in 2004. Why Is It Still in Use?",{"type":8,"value":4662,"toc":4962},[4663,4666,4670,4673,4683,4686,4692,4696,4705,4714,4723,4727,4730,4733,4736,4740,4749,4758,4767,4771,4774,4783,4790,4794,4803,4810,4818,4836,4840,4923,4927,4934,4954,4957,4959],[11,4664,4665],{},"Hash functions sit underneath digital signatures, certificates, version-control systems, and downloaded-file verification, yet most people only notice them when a practical attack makes the news. The history of MD5 and SHA-1 is therefore not just a story about better cryptanalysis; it also shows how slowly an industry can retire an algorithm after its limitations become common knowledge.",[15,4667,4669],{"id":4668},"what-it-means-for-a-hash-function-to-break","What it means for a hash function to break",[11,4671,4672],{},"Saying that \"MD5 is broken\" can sound as though anyone can now take an arbitrary MD5 digest and instantly recover the original file or password. That is not what happened. Cryptographic hash functions are expected to satisfy several independent properties, and an attack against one of them does not automatically solve the others.",[11,4674,4675,4678,4679,4682],{},[26,4676,4677],{},"Collision resistance"," means that an attacker should not be able to construct two different messages with the same digest. This is the property that MD5 and SHA-1 lost. ",[26,4680,4681],{},"Preimage resistance"," asks a different question: given one digest, can an attacker find a message that produces it? Password cracking is different again because it usually targets a database of fast, poorly protected password hashes rather than the abstract preimage problem. Collision attacks do not reveal a password automatically, but the speed of MD5 and SHA-1 still makes both unsuitable for password storage.",[11,4684,4685],{},"Those distinctions matter in practice. A file checksum, a digital signature, and a password database all use hashes for different purposes, so the single word \"broken\" does not describe the risk in each setting.",[11,4687,4688],{},[1068,4689],{"alt":4690,"src":4691},"SHA-1 hash collision between two distinct documents and the timeline of hashing standards","\u002Fimages\u002Fblog\u002Finline_hash-generator.webp",[15,4693,4695],{"id":4694},"md5-from-a-cautious-md4-successor-to-a-rogue-certificate-authority","MD5: from a cautious MD4 successor to a rogue certificate authority",[11,4697,4698,4699,4704],{},"Ronald Rivest designed MD5 in 1991 as a more conservative successor to MD4. Published in April 1992, ",[38,4700,4703],{"href":4701,"rel":4702},"https:\u002F\u002Fwww.rfc-editor.org\u002Finfo\u002Frfc1321\u002F",[429],"RFC 1321"," explicitly noted that MD4 was being adopted faster than it could be thoroughly reviewed, so MD5 gave up some speed in exchange for a larger safety margin. At the time, its compact 128-bit fingerprint looked like a reasonable engineering choice, and the algorithm spread through applications, protocols, and public-key infrastructure.",[11,4706,4707,4708,4713],{},"Warnings appeared well before the final practical exploit, but 2004 was the turning point. Xiaoyun Wang, Dengguo Feng, Xuejia Lai, and Hongbo Yu published the first pairs of messages with identical MD5 digests, followed by a detailed attack in 2005. Researchers then moved beyond making collision searches faster and started learning how to place useful structure around the calculated collision blocks. ",[38,4709,4712],{"href":4710,"rel":4711},"https:\u002F\u002Fwww.rfc-editor.org\u002Finfo\u002Frfc6151\u002F",[429],"RFC 6151"," summarizes that progression and ultimately declares MD5 unacceptable wherever collision resistance is required.",[11,4715,4716,4717,4722],{},"Chosen-prefix collisions proved particularly important. Starting from two deliberately prepared documents, an attacker could calculate different appendages that made the final MD5 digests equal. In 2008, Marc Stevens and his collaborators used that technique against a commercial certificate authority and created a ",[38,4718,4721],{"href":4719,"rel":4720},"https:\u002F\u002Fmarc-stevens.nl\u002Fresearch\u002Fhashclash\u002Frogue-ca\u002Findex.html",[429],"rogue intermediate CA certificate",". Browsers trusted the original root, so they would also trust certificates issued through the forged intermediate. The demonstration turned an abstract weakness into a direct attack on the web's trust model.",[15,4724,4726],{"id":4725},"why-md5-has-not-disappeared","Why MD5 has not disappeared",[11,4728,4729],{},"A vulnerable algorithm rarely vanishes at once. A digest may already be stored in billions of objects, embedded in a file format, or fixed by an interoperability protocol. Replacing it can require every participant to upgrade, old values to be recomputed, and archival data to remain readable. That is why MD5 still appears in long-lived software, package repositories, integrations, and databases created many years ago.",[11,4731,4732],{},"There is also a narrower use case: detecting accidental corruption in an environment where an adversary cannot choose the input. Cryptographic collision resistance is unnecessary for that specific job, so some systems retain MD5 as a familiar and universally available checksum. The boundary is crucial, however. Once an untrusted party can prepare a file deliberately, MD5 is inappropriate for signatures, authenticity checks, content-addressed storage, or any decision where an equal digest is treated as proof of equal content.",[11,4734,4735],{},"Password storage fails for a different reason. Even without considering collisions, MD5 and SHA-1 are far too fast, allowing an attacker to test enormous numbers of guesses per second. Passwords need a deliberately expensive password-hashing or key-derivation function such as Argon2id, scrypt, bcrypt, or PBKDF2, together with a unique salt and appropriate cost parameters.",[15,4737,4739],{"id":4738},"sha-1-the-warning-came-before-the-public-collision","SHA-1: the warning came before the public collision",[11,4741,4742,4743,4748],{},"NIST standardized SHA-1 in 1995 as a corrected successor to SHA-0. New cryptanalytic results in 2005 substantially reduced the expected work needed to find a collision, and by 2006 NIST was already ",[38,4744,4747],{"href":4745,"rel":4746},"https:\u002F\u002Fcsrc.nist.gov\u002FNews\u002F2022\u002Fnist-transitioning-away-from-sha-1-for-all-apps",[429],"encouraging a rapid transition to SHA-2"," for digital signatures. No complete public SHA-1 collision existed yet, but the original security margin was clearly gone.",[11,4750,4751,4752,4757],{},"The practical endpoint arrived in 2017 when Google and CWI Amsterdam announced ",[38,4753,4756],{"href":4754,"rel":4755},"https:\u002F\u002Fshattered.io\u002F",[429],"SHAttered",", the first public collision for the full SHA-1 function. The researchers created two different PDF files with the same digest after roughly nine quintillion SHA-1 computations. The project used work equivalent to about 6,500 CPU-years for its first phase and 110 GPU-years for its second. That was still expensive, but it was within reach of a well-funded organization and removed any remaining case for treating SHA-1 as a sound basis for digital signatures.",[11,4759,4760,4761,4766],{},"Migration still took years because standards, certificates, devices, and archival formats move at different speeds. The twelve-year gap between the first major cryptanalytic warning and SHAttered illustrates that delay, while ",[38,4762,4765],{"href":4763,"rel":4764},"https:\u002F\u002Fcsrc.nist.gov\u002Fprojects\u002Fhash-functions\u002Fnist-policy-on-hash-functions",[429],"NIST's continuing SHA-1 transition policy"," shows that retirement work continues even after a public collision.",[15,4768,4770],{"id":4769},"sha-2-and-sha-3-are-not-two-stages-of-one-mandatory-upgrade","SHA-2 and SHA-3 are not two stages of one mandatory upgrade",[11,4772,4773],{},"The SHA-2 family entered the NIST standard in 2002 and includes SHA-224, SHA-256, SHA-384, and SHA-512. SHA-256 remains the most common general-purpose choice for new systems: there are no known practical attacks against the full function, implementation support is broad, and it is suitable for digital signatures, file fingerprints, and other applications that require a modern cryptographic hash.",[11,4775,4776,4777,4782],{},"After the attacks on MD5 and SHA-1, NIST ran an open competition for an algorithm whose design did not repeat SHA-2. Keccak, designed by Guido Bertoni, Joan Daemen, Michaël Peeters, and Gilles Van Assche, won in 2012 and became SHA-3 in FIPS 202 in 2015. NIST nevertheless ",[38,4778,4781],{"href":4779,"rel":4780},"https:\u002F\u002Fwww.nist.gov\u002Fnews-events\u002Fnews\u002F2015\u002F08\u002Fnist-releases-sha-3-cryptographic-hash-standard",[429],"states explicitly"," that SHA-2 remains secure and viable. The higher number does not make SHA-3 a required replacement for SHA-256; it gives the industry an alternative based on different internal mathematics.",[11,4784,4785,4786,4789],{},"The design difference also affects how the functions may be used. A homemade ",[49,4787,4788],{},"SHA256(secret || message)"," construction is vulnerable to a length-extension attack because SHA-256 lets an attacker continue hashing from the published state. SHA-3's sponge construction does not have that specific weakness, but message authentication should still use a standard keyed construction rather than a custom concatenation of secret and data.",[15,4791,4793],{"id":4792},"hmac-why-a-hash-sometimes-needs-a-key","HMAC: why a hash sometimes needs a key",[11,4795,4796,4797,4802],{},"Anyone can compute an ordinary hash, so matching it against a trusted value can confirm data integrity but says nothing about who produced it. HMAC adds a shared secret and produces a message authentication code that another holder of the same secret can verify. Mihir Bellare, Ran Canetti, and Hugo Krawczyk standardized the construction in ",[38,4798,4801],{"href":4799,"rel":4800},"https:\u002F\u002Fwww.rfc-editor.org\u002Finfo\u002Frfc2104\u002F",[429],"RFC 2104"," in 1997.",[11,4804,4805,4806,4809],{},"HMAC performs two nested hash operations instead of using a simple ",[49,4807,4808],{},"hash(key + message)"," formula:",[1564,4811,4816],{"className":4812,"code":4814,"language":4815},[4813],"language-text","HMAC(K, m) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || m ) )\n","text",[49,4817,4814],{"__ignoreMap":363},[11,4819,4820,4821,4824,4825,1121,4828,4831,4832,4835],{},"Here ",[49,4822,4823],{},"K'"," is the key normalized to the hash function's block size, ",[49,4826,4827],{},"ipad",[49,4829,4830],{},"opad"," are fixed padding values, and ",[49,4833,4834],{},"||"," means concatenation. The inner operation binds the message to the key; the outer layer hides the intermediate state and prevents the ordinary length-extension technique from continuing it. New systems typically use HMAC-SHA-256 or HMAC-SHA-512.",[15,4837,4839],{"id":4838},"what-to-choose-today","What to choose today",[75,4841,4842,4855],{},[78,4843,4844],{},[81,4845,4846,4849,4852],{},[84,4847,4848],{},"Task",[84,4850,4851],{},"Appropriate choice",[84,4853,4854],{},"Important limitation",[91,4856,4857,4868,4879,4890,4901,4912],{},[81,4858,4859,4862,4865],{},[96,4860,4861],{},"Cryptographic fingerprint of a file or message",[96,4863,4864],{},"SHA-256",[96,4866,4867],{},"A broadly supported modern default",[81,4869,4870,4873,4876],{},[96,4871,4872],{},"Independent design or SHA-3 interoperability",[96,4874,4875],{},"SHA3-256",[96,4877,4878],{},"Not a mandatory upgrade from SHA-256",[81,4880,4881,4884,4887],{},[96,4882,4883],{},"Authenticate a webhook or API request with a shared secret",[96,4885,4886],{},"HMAC-SHA-256",[96,4888,4889],{},"Compare authentication codes without leaking timing information",[81,4891,4892,4895,4898],{},[96,4893,4894],{},"Store passwords",[96,4896,4897],{},"Argon2id, scrypt, bcrypt, or PBKDF2",[96,4899,4900],{},"Plain MD5, SHA-1, and SHA-256 are too fast for this job",[81,4902,4903,4906,4909],{},[96,4904,4905],{},"Detect accidental corruption in a trusted environment",[96,4907,4908],{},"CRC32 or an existing legacy checksum",[96,4910,4911],{},"Not evidence of authenticity",[81,4913,4914,4917,4920],{},[96,4915,4916],{},"Read an old format that mandates a legacy digest",[96,4918,4919],{},"MD5 or SHA-1 only as required",[96,4921,4922],{},"Do not carry the old algorithm into a new protocol or signature",[15,4924,4926],{"id":4925},"what-our-hash-generator-does","What our hash generator does",[11,4928,510,4929,4933],{},[38,4930,4932],{"href":4931},"\u002Ftools\u002Fhash-generator","Hash Generator"," computes digests in your browser without sending the entered text or selected file to a server. MD5 and SHA-1 remain available for old data and interoperability, while new work can use modern options:",[298,4935,4936,4939,4942,4945,4948,4951],{},[301,4937,4938],{},"SHA-256, SHA-384, and SHA-512 from the SHA-2 family;",[301,4940,4941],{},"SHA3-256 and SHA3-512 using Keccak's sponge construction;",[301,4943,4944],{},"HMAC for authenticating a message with a secret key;",[301,4946,4947],{},"CRC32 for non-cryptographic accidental-error detection;",[301,4949,4950],{},"local file hashing and comparison with a known digest;",[301,4952,4953],{},"MD5 and SHA-1 for legacy cases in which an existing format requires them.",[11,4955,4956],{},"For a quick comparison, enter \"Hello, world,\" compare MD5's 32 hexadecimal characters with SHA-256's 64, then enable HMAC and add a key. The final value can no longer be reproduced from the message alone because calculating it now requires the same secret.",[15,4958,358],{"id":357},[357,4960],{":slug":4961},"[\"hash-generator\",\"jwt-inspector\",\"uuid-generator\",\"encryption-toolbox\",\"webhook-verifier\",\"telegram-webhook-tester\"]",{"title":363,"searchDepth":364,"depth":364,"links":4963},[4964,4965,4966,4967,4968,4969,4970,4971,4972],{"id":4668,"depth":364,"text":4669},{"id":4694,"depth":364,"text":4695},{"id":4725,"depth":364,"text":4726},{"id":4738,"depth":364,"text":4739},{"id":4769,"depth":364,"text":4770},{"id":4792,"depth":364,"text":4793},{"id":4838,"depth":364,"text":4839},{"id":4925,"depth":364,"text":4926},{"id":357,"depth":364,"text":358},"2026-08-17","What MD5 and SHA-1 collisions actually broke, why old hashes still appear in production, and when to choose SHA-2, SHA-3, CRC32 or HMAC.",[4976,4979,4982,4985,4988],{"q":4977,"a":4978},"Who broke MD5 and when?","Xiaoyun Wang and her coauthors published the first pairs of messages with the same MD5 digest in 2004, then described the attack in detail in 2005. Marc Stevens later developed chosen-prefix collision techniques and HashClash, which gave MD5 collisions practical applications.",{"q":4980,"a":4981},"Why are MD5 and SHA-1 still in use?","They remain embedded in old protocols, data formats, and integrations where changing the algorithm would break compatibility. MD5 also survives as a quick check for accidental corruption of trusted data. Neither MD5 nor SHA-1 should be chosen for digital signatures, certificates, new protocols, or password storage.",{"q":4983,"a":4984},"How is SHA-3 different from SHA-2?","The families use different internal designs. SHA-256 and SHA-512 use the iterative structure of SHA-2, while SHA-3 is based on Keccak's sponge construction. SHA-3 complements SHA-2 with an independent design; a system does not need to replace a secure SHA-256 implementation merely because SHA-3 has a higher number.",{"q":4986,"a":4987},"What is HMAC and why do I need it?","HMAC combines a hash function with a secret key. It detects message changes and verifies that the authentication code was created by a party that knows that key. RFC 2104 standardizes the construction and avoids the weaknesses of a homemade hash(key + message) scheme.",{"q":4989,"a":4990},"Why isn't hashing the same as encryption?","Encryption must be reversible for someone who holds the key, while a cryptographic hash produces a one-way fingerprint of data. A hash can help verify integrity, but it does not conceal the original message and cannot replace encryption.","\u002Fimages\u002Fblog\u002Fblog_hash-generator.webp",{},"\u002Fblog\u002Fen\u002Fhash-generator",{"title":4660,"description":4974},"blog\u002Fen\u002Fhash-generator",[4997,4998,4999,5000,5001,5002],"MD5 collisions","hash function history","SHA-1 vs SHA-256","why use HMAC","Keccak SHA-3","Wang Xiaoyun","TnXJIe0A4OCGyymi2PxYgDEUzY3m2J-emWGFgxYjc4U",{"id":5005,"title":5006,"body":5007,"date":5305,"description":5306,"extension":376,"faq":5307,"image":5323,"lang":394,"meta":5324,"navigation":396,"path":5325,"published":396,"readTime":1270,"seo":5326,"stem":5327,"tags":5328,"updated":405,"__hash__":5333},"blog\u002Fblog\u002Fen\u002Fkey-pair-generator.md","RSA or Ed25519: Which SSH Key Should You Choose?",{"type":8,"value":5008,"toc":5297},[5009,5020,5024,5037,5053,5056,5059,5063,5076,5089,5096,5100,5115,5118,5121,5125,5141,5161,5164,5253,5256,5260,5276,5286,5292,5294],[11,5010,5011,5012,5015,5016,5019],{},"Advice to replace ",[49,5013,5014],{},"ssh-keygen -t rsa"," with ",[49,5017,5018],{},"ssh-keygen -t ed25519"," is often reduced to one sentence: Ed25519 is newer, shorter, and faster. That explanation misses the important parts. Key lengths from different cryptosystems cannot be compared directly, RSA did not become unsafe on a single date, and some reports that “RSA was disabled” actually refer only to old RSA signatures using SHA-1. Choosing an SSH key depends on mathematics, but also on server versions, hardware tokens, and the rules of the infrastructure where the key will be used.",[15,5021,5023],{"id":5022},"rsa-made-public-key-cryptography-practical","RSA made public-key cryptography practical",[11,5025,5026,5027,5030,5031,5036],{},"Whitfield Diffie and Martin Hellman published the idea of public-key cryptography in 1976, but their work did not provide a general ready-to-use system for both encryption and signatures. The following year, MIT researchers ",[26,5028,5029],{},"Ron Rivest, Adi Shamir, and Leonard Adleman"," proposed the method named from their initials. Their paper, ",[38,5032,5035],{"href":5033,"rel":5034},"https:\u002F\u002Fpeople.csail.mit.edu\u002Frivest\u002FRsapaper.pdf",[429],"“A Method for Obtaining Digital Signatures and Public-Key Cryptosystems”",", appeared in Communications of the ACM in February 1978.",[11,5038,5039,5040,1121,5042,5045,5046,5049,5050,5052],{},"RSA starts with two large primes, ",[49,5041,11],{},[49,5043,5044],{},"q",", whose product becomes the modulus ",[49,5047,5048],{},"n",". Multiplying chosen primes is easy, while recovering them from a sufficiently large ",[49,5051,5048],{}," is believed to be computationally difficult. That asymmetry allows one part of the key to be published while the other remains secret.",[11,5054,5055],{},"Textbook explanations often reduce RSA to modular exponentiation, but that operation alone does not form a secure modern system. Encryption needs an encoding scheme such as OAEP, while signatures combine hashing with a defined signature construction such as PSS or PKCS #1 v1.5. In SSH, an RSA key normally does not encrypt the whole connection. The client signs session data to prove possession of the private key, while separately negotiated symmetric keys protect the actual traffic.",[11,5057,5058],{},"RSA has survived for more reasons than age and familiarity. Its formats are supported by older SSH servers, network appliances, smart cards, HSMs, and enterprise policies whose approved algorithm lists change slowly. RSA with a suitable modulus and modern signature algorithm remains usable, although its keys and signatures are much larger and its generation and private-key operations are more expensive than those of modern elliptic-curve schemes.",[15,5060,5062],{"id":5061},"from-curve25519-to-ed25519-without-confusing-their-jobs","From Curve25519 to Ed25519 without confusing their jobs",[11,5064,5065,5066,5069,5070,5075],{},"In 2006, ",[26,5067,5068],{},"Daniel J. Bernstein"," introduced ",[38,5071,5074],{"href":5072,"rel":5073},"https:\u002F\u002Fcr.yp.to\u002Fecdh\u002Fcurve25519-20060209.pdf",[429],"Curve25519"," as a fast and implementation-friendly primitive for Diffie-Hellman shared-secret agreement. The corresponding function is now commonly called X25519. It helps two peers derive a shared secret, but does not itself create digital signatures or replace an authentication algorithm.",[11,5077,5078,5079,5084,5085,5088],{},"Five years later, Bernstein, Niels Duif, Tanja Lange, Peter Schwabe, and Bo-Yin Yang published ",[38,5080,5083],{"href":5081,"rel":5082},"https:\u002F\u002Fed25519.cr.yp.to\u002Fed25519-20110926.pdf",[429],"“High-speed high-security signatures”",", describing Ed25519 as a concrete form of EdDSA. Ed25519 uses the twisted Edwards curve edwards25519 over the field defined by the prime ",[49,5086,5087],{},"2²⁵⁵ − 19",". That curve is equivalent to Curve25519 under a change of coordinates, so their discrete-logarithm problems are related, but the protocols and key encodings remain distinct.",[11,5090,5091,5092,5095],{},"The distinction matters in practice. X25519 establishes a secret that can later protect data, while Ed25519 signs a message and lets someone verify the signature with a public key. An ",[49,5093,5094],{},"ssh-ed25519"," line cannot simply be used as an X25519 key without a defined conversion and protocol, just as a signature does not replace key agreement because both names contain 25519.",[15,5097,5099],{"id":5098},"what-ed25519-improved-for-signatures","What Ed25519 improved for signatures",[11,5101,5102,5103,5108,5109,5114],{},"Published in January 2017, ",[38,5104,5107],{"href":5105,"rel":5106},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc8032",[429],"RFC 8032"," describes EdDSA, the Ed25519 parameters, and test vectors. An Ed25519 public key encodes to 32 bytes, a signature occupies 64 bytes, and the scheme targets about 128 bits of classical security. In the comparable-strength table in ",[38,5110,5113],{"href":5111,"rel":5112},"https:\u002F\u002Fdoi.org\u002F10.6028\u002FNIST.SP.800-57pt1r5",[429],"NIST SP 800-57 Part 1 Rev. 5",", RSA needs a 3072-bit modulus for the same approximate 128-bit level, while RSA-2048 is assessed at roughly 112 bits. Calling Ed25519 “equivalent to RSA-4096” is therefore at most a rough shorthand, not a precise equality.",[11,5116,5117],{},"Ed25519 also avoids requiring a fresh random nonce for every signature because that value is derived deterministically from the private key and message. This removes a class of failures in which poor signing-time randomness exposed private keys in other schemes. Complete point-addition formulas also make it easier to write code without exceptional branches for special points.",[11,5119,5120],{},"The algorithm does not make every implementation automatically resistant to side channels. RFC 8032 still instructs implementers to avoid branches and memory accesses that depend on secret values. Ed25519's design makes constant-time code easier, but a concrete library must do that work correctly. Performance comparisons need similar care: Ed25519 key generation and signing are generally fast and predictable, while RSA verification with a small public exponent can also be extremely fast. No algorithm wins every operation on every platform.",[15,5122,5124],{"id":5123},"what-actually-changed-in-openssh","What actually changed in OpenSSH",[11,5126,5127,5132,5133,5140],{},[38,5128,5131],{"href":5129,"rel":5130},"https:\u002F\u002Fwww.openssh.com\u002Ftxt\u002Frelease-6.5",[429],"OpenSSH 6.5",", released on January 30, 2014, added Ed25519 as a user and host key type. The current ",[38,5134,5137,1921],{"href":5135,"rel":5136},"https:\u002F\u002Fman.openbsd.org\u002Fssh-keygen",[429],[49,5138,5139],{},"ssh-keygen"," identifies Ed25519 as the default. It is a practical choice for a new key when both ends run modern implementations, but old servers and closed network devices still require a compatibility check.",[11,5142,5143,5144,5149,5150,5153,5154,698,5157,5160],{},"RSA followed a different transition. OpenSSH 7.2 added RSA signatures using SHA-256 and SHA-512 in 2016, then ",[38,5145,5148],{"href":5146,"rel":5147},"https:\u002F\u002Fwww.openssh.com\u002Ftxt\u002Frelease-8.8",[429],"OpenSSH 8.8"," disabled the SHA-1-based ",[49,5151,5152],{},"ssh-rsa"," signature algorithm by default in 2021. Its release notes explicitly say that most users do not need to replace existing RSA keys: the same key automatically uses ",[49,5155,5156],{},"rsa-sha2-256",[49,5158,5159],{},"rsa-sha2-512"," when the other side supports them.",[11,5162,5163],{},"The practical rule is consequently straightforward. Start with Ed25519 for new access between modern systems. Use RSA when the target server, token, or policy lacks Ed25519 support, choose a sufficient modulus, and confirm that the connection negotiates RSA\u002FSHA-2 instead of restoring RSA\u002FSHA-1 merely to accommodate obsolete software.",[75,5165,5166,5182],{},[78,5167,5168],{},[81,5169,5170,5173,5176,5179],{},[84,5171,5172],{},"Criterion",[84,5174,5175],{},"Ed25519",[84,5177,5178],{},"RSA-2048",[84,5180,5181],{},"RSA-4096",[91,5183,5184,5197,5211,5225,5239],{},[81,5185,5186,5189,5192,5195],{},[96,5187,5188],{},"Underlying problem",[96,5190,5191],{},"Elliptic-curve discrete logarithm",[96,5193,5194],{},"Factoring a large modulus",[96,5196,5194],{},[81,5198,5199,5202,5205,5208],{},[96,5200,5201],{},"Estimated classical strength",[96,5203,5204],{},"About 128 bits",[96,5206,5207],{},"About 112 bits",[96,5209,5210],{},"Above the 128-bit RSA-3072 reference point, below the 192-bit RSA-7680 point",[81,5212,5213,5216,5219,5222],{},[96,5214,5215],{},"Main public value",[96,5217,5218],{},"32 bytes",[96,5220,5221],{},"256-byte modulus",[96,5223,5224],{},"512-byte modulus",[81,5226,5227,5230,5233,5236],{},[96,5228,5229],{},"Generation and private operations",[96,5231,5232],{},"Usually fast",[96,5234,5235],{},"Slower than Ed25519",[96,5237,5238],{},"Considerably heavier than RSA-2048",[81,5240,5241,5244,5247,5250],{},[96,5242,5243],{},"SSH compatibility",[96,5245,5246],{},"Modern OpenSSH and many current clients",[96,5248,5249],{},"Very broad, including older systems",[96,5251,5252],{},"Same algorithm family, with larger keys and more expensive operations",[11,5254,5255],{},"The sizes in the table describe the raw public value or modulus. An OpenSSH line, private-key container, and surrounding format data always occupy more space. Neither RSA nor Ed25519 is post-quantum: a sufficiently capable quantum computer threatens both mathematical problems, so moving between these two algorithms does not solve post-quantum migration.",[15,5257,5259],{"id":5258},"what-the-browser-key-pair-generator-does","What the browser Key Pair Generator does",[11,5261,5262,5266,5267,698,5269,5271,5272,5275],{},[38,5263,5265],{"href":5264},"\u002Ftools\u002Fkey-pair-generator","Key Pair Generator"," calls the Web Crypto API directly in the open page and offers Ed25519, RSA-2048, or RSA-4096. For RSA, the implementation creates an RSA-PSS key with SHA-256 and public exponent 65537. It then assembles an ",[49,5268,5094],{},[49,5270,5152],{}," public line, calculates the familiar ",[49,5273,5274],{},"SHA256"," fingerprint over the SSH blob, and lets the user copy or download the result.",[11,5277,5278,5279,5282,5283,5285],{},"The private part is exported as unencrypted ",[26,5280,5281],{},"PKCS#8 PEM",". This is not the private-key container normally written by ",[49,5284,5139],{},", so the private file's compatibility must be checked in the target program; the OpenSSH public line and the PKCS#8 private container serve different consumers. Keys exist only in page memory, and the current implementation neither transmits them to a server nor writes them to storage. Generating again replaces the previous pair.",[11,5287,5288,5289,5291],{},"Those properties make the tool useful for learning the formats, comparing sizes, and creating temporary keys in a compatible environment, but they do not remove the need for caution. The unencrypted private key is displayed on screen and downloaded without a passphrase, while online generation also depends on the integrity of the page code, browser, and device. For persistent access to production servers, prefer running ",[49,5290,5018],{}," locally, protect the file with a passphrase, or use a hardware-backed key. If RSA compatibility is required, choose its parameters from the target system's requirements rather than from the apparent length of a generated string.",[15,5293,358],{"id":357},[357,5295],{":slug":5296},"[\"key-pair-generator\",\"shamir-secret\",\"jwt-inspector\",\"encryption-toolbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":5298},[5299,5300,5301,5302,5303,5304],{"id":5022,"depth":364,"text":5023},{"id":5061,"depth":364,"text":5062},{"id":5098,"depth":364,"text":5099},{"id":5123,"depth":364,"text":5124},{"id":5258,"depth":364,"text":5259},{"id":357,"depth":364,"text":358},"2026-08-16","How RSA, Curve25519, and Ed25519 emerged, how their security models differ, and when SSH still needs RSA compatibility.",[5308,5311,5314,5317,5320],{"q":5309,"a":5310},"Should I choose RSA or Ed25519 for a new SSH key?","If every required client, server, and device supports Ed25519, it is usually a convenient choice for a new key: compact encoding, fast generation, and roughly 128-bit classical security. RSA remains reasonable for older systems, hardware tokens, and environments that permit only a limited set of algorithms.",{"q":5312,"a":5313},"Is RSA considered broken?","No. RSA of sufficient length with a modern signature scheme remains usable. In SSH, an RSA key should not be confused with the old SHA-1-based ssh-rsa signature algorithm: OpenSSH 8.8 disabled RSA\u002FSHA-1, while the same RSA key can use rsa-sha2-256 or rsa-sha2-512.",{"q":5315,"a":5316},"Are Ed25519 and Curve25519 the same algorithm?","No. Curve25519 was designed for establishing a shared secret, while Ed25519 is a digital signature scheme. They operate over the same field and use equivalent curves in different coordinate forms, but their protocols, key formats, and purposes are different.",{"q":5318,"a":5319},"When did OpenSSH add Ed25519 support?","OpenSSH 6.5, released in January 2014, added Ed25519 for user and host keys. The current ssh-keygen manual identifies Ed25519 as the default type, although versions shipped by older operating systems and network appliances may behave differently.",{"q":5321,"a":5322},"Should I use the browser Key Pair Generator for a production SSH key?","The tool does not transmit or store keys, but it displays an unencrypted PKCS#8 private key rather than the usual OpenSSH private-key container. For long-lived production access, prefer local ssh-keygen with a passphrase or a hardware-backed key; the browser generator is useful for learning formats, testing, and compatible temporary scenarios.","\u002Fimages\u002Fblog\u002Fblog_key-pair-generator.webp",{},"\u002Fblog\u002Fen\u002Fkey-pair-generator",{"title":5006,"description":5306},"blog\u002Fen\u002Fkey-pair-generator",[5329,5330,5074,5331,5175,5332],"RSA and Ed25519","RSA history","SSH keys","OpenSSH","nNnh7-XVdLHFKbMwi47cachZZnJJuODvKXbqCCR3VwY",{"id":5335,"title":5336,"body":5337,"date":5532,"description":5533,"extension":376,"faq":5534,"image":5550,"lang":394,"meta":5551,"navigation":396,"path":5552,"published":396,"readTime":1591,"seo":5553,"stem":5554,"tags":5555,"updated":405,"__hash__":5562},"blog\u002Fblog\u002Fen\u002Fpassphrase-generator.md","How Diceware Turned Random Words into Strong Passphrases",{"type":8,"value":5338,"toc":5524},[5339,5342,5351,5355,5370,5373,5383,5388,5407,5413,5417,5433,5440,5443,5447,5456,5459,5468,5472,5481,5484,5488,5499,5506,5516,5519,5521],[11,5340,5341],{},"A long passphrase does not become strong merely because it contains many letters and spaces. People choose quotations, lyrics, and appealing sentences according to familiar patterns, and attackers can incorporate those patterns into their guessing dictionaries. Diceware derives its strength from something else: the words are selected by a source of randomness, so an attacker must search the full space of possible combinations.",[11,5343,5344,5345,5350],{},"This idea predates the four words that became an internet meme through an XKCD comic. In the mid-1990s, Arnold Reinhold proposed using ordinary dice as a transparent source of randomness that did not depend on a computer. The method became known as ",[38,5346,5349],{"href":5347,"rel":5348},"https:\u002F\u002Ftheworld.com\u002F~reinhold\u002Fdiceware.html",[429],"Diceware",", connecting a simple physical procedure to a measurable amount of uncertainty.",[15,5352,5354],{"id":5353},"how-five-rolls-become-one-word","How five rolls become one word",[11,5356,5357,5358,5361,5362,5365,5366,5369],{},"The classic Diceware list has exactly ",[26,5359,5360],{},"7,776 entries"," because five rolls of a six-sided die produce that many sequences: ",[49,5363,5364],{},"6⁵ = 7,776",". You write the results in order, such as ",[49,5367,5368],{},"43463",", look up that row in the list, and receive one word. Another five rolls select the next word, and the process continues until the passphrase reaches the desired length.",[11,5371,5372],{},"It is important not to choose the most appealing result from several attempts or reroll because a combination looks odd. Repeated words are valid as well. Every choice needs to remain independent and uniform; otherwise, the actual set of possibilities shrinks and the familiar entropy calculation no longer describes the resulting phrase.",[11,5374,5375,5376,5379,5380,5382],{},"For a wordlist of size ",[49,5377,5378],{},"W"," and a phrase containing ",[49,5381,5048],{}," independently selected words, the entropy is:",[11,5384,5385],{},[49,5386,5387],{},"H = n × log₂(W)",[11,5389,5390,5391,5394,5395,5398,5399,5402,5403,5406],{},"When ",[49,5392,5393],{},"W = 7,776",", one word contributes about ",[26,5396,5397],{},"12.9 bits",", five words provide roughly ",[26,5400,5401],{},"64.6 bits",", and six provide about ",[26,5404,5405],{},"77.5 bits",". This is not a timer promising a particular number of years before a password is cracked. Guessing speed depends on whether a service limits attempts, how it hashes passwords, and whether an attacker has obtained a database for offline cracking. Entropy describes the number of equally likely candidates before those conditions enter the calculation.",[11,5408,5409],{},[1068,5410],{"alt":5411,"src":5412},"Five dice rolls form an index that independently selects a word from the Diceware list","\u002Fimages\u002Fblog\u002Finline_passphrase-generator.webp",[15,5414,5416],{"id":5415},"what-xkcd-936-changed","What XKCD 936 changed",[11,5418,5419,5420,5425,5426,5015,5429,5432],{},"In 2011, Randall Munroe published ",[38,5421,5424],{"href":5422,"rel":5423},"https:\u002F\u002Fxkcd.com\u002F936\u002F",[429],"XKCD #936, “Password Strength”",", comparing ",[49,5427,5428],{},"Tr0ub4dor&3",[49,5430,5431],{},"correct horse battery staple",". The first password looks complicated, but it is built from a familiar word, a common letter-to-digit substitution, and a predictable suffix. The second is easier to picture and remember, while its words are independently selected in the comic's model.",[11,5434,5435,5436,5439],{},"The comic's estimates of 28 and 44 bits are often repeated as if they were ready-made measurements, although they only make sense under Munroe's assumptions about how each password was created. Four words selected uniformly from the complete Diceware list would yield about ",[26,5437,5438],{},"51.7 bits",", but the same number cannot be assigned to a four-word sentence composed by a person. The comic's famous phrase itself has also been in password-cracking dictionaries for years and should never be treated as a secret.",[11,5441,5442],{},"XKCD's main contribution was not a new formula but a memorable explanation of the tradeoff between strength and memorability. It showed a broad audience why visual complexity is not the same thing as randomness at the point of creation.",[15,5444,5446],{"id":5445},"why-eff-published-a-new-list","Why EFF published a new list",[11,5448,5449,5450,5455],{},"Reinhold's original list favored short entries, so it included abbreviations and character sequences that were not always easy to pronounce. In 2016, the Electronic Frontier Foundation published a ",[38,5451,5454],{"href":5452,"rel":5453},"https:\u002F\u002Fwww.eff.org\u002Fdeeplinks\u002F2016\u002F07\u002Fnew-wordlists-random-passphrases",[429],"long list of 7,776 more recognizable English words",". The size of the space stayed the same, so each random word retained the same 12.9 bits of entropy, but the resulting phrases became easier to read and memorize.",[11,5457,5458],{},"EFF recommends six random words for most purposes and specifically warns against reusing the resulting phrase across websites. A password manager is more practical for generating unique credentials for ordinary accounts. Diceware is most useful when one secret genuinely needs to be remembered, such as a password manager's master password, a disk encryption password, or the passphrase that unlocks a private key.",[11,5460,5461,5462,5467],{},"Current NIST guidance supports the broad principle without declaring every phrase secure. ",[38,5463,5466],{"href":5464,"rel":5465},"https:\u002F\u002Fpages.nist.gov\u002F800-63-4\u002Fsp800-63b.html",[429],"SP 800-63B-4"," tells services to permit long passwords, spaces, and pasting from password managers, while checking new passwords against a blocklist of common or compromised values. It also rejects mandatory composition rules such as requiring one uppercase letter, one digit, and one special character because people satisfy them in predictable ways. Length helps, but the selection process remains decisive.",[15,5469,5471],{"id":5470},"why-bip39-is-a-different-system","Why BIP39 is a different system",[11,5473,5474,5475,5480],{},"A set of 12 or 24 wallet words may look like Diceware, but it solves another problem. A ",[38,5476,5479],{"href":5477,"rel":5478},"https:\u002F\u002Fgithub.com\u002Fbitcoin\u002Fbips\u002Fblob\u002Fmaster\u002Fbip-0039.mediawiki",[429],"BIP39 mnemonic"," encodes 128 to 256 bits of computer-generated entropy plus a checksum, then splits that data into indices for a 2,048-word list. It is not a password sentence that should be invented or assembled in a general passphrase generator.",[11,5482,5483],{},"BIP39 also defines an input called a passphrase, but it is supplied to the seed derivation function separately from the mnemonic. A Diceware phrase can therefore serve as an additional secret for a compatible wallet when used carefully, but it cannot replace the recovery words.",[15,5485,5487],{"id":5486},"how-the-generator-on-this-site-works","How the generator on this site works",[11,5489,510,5490,5494,5495,5498],{},[38,5491,5493],{"href":5492},"\u002Ftools\u002Fpassphrase-generator","Passphrase Generator"," offers four wordlists. EFF's modern English list is the default, while the original Diceware list, the alternative Beale list, and a Russian Diceware list are also available. Each file contains ",[26,5496,5497],{},"7,776 unique words",", so uniform selection provides the same 12.9 bits per word in every case. The choice changes the language, vocabulary, and memorability of the result, but not the size of the combination space.",[11,5500,5501,5502,5505],{},"Every word is selected independently with ",[49,5503,5504],{},"crypto.getRandomValues",". The generator preserves a uniform distribution through rejection sampling, discarding values that would introduce a slight modulo bias when mapped to a wordlist index. It also verifies that a loaded file contains exactly 7,776 unique words, so an incomplete or damaged list is not used for generation.",[11,5507,5508,5509,5512,5513,507],{},"The tool offers 6, 12, 15, 18, or 24 words, several case styles, and spaces, hyphens, underscores, or a custom separator. Case and separator choices contribute no entropy because the user sets them and an attacker can assume them. The optional random digit suffix adds ",[49,5510,5511],{},"log₂(10)",", or approximately ",[26,5514,5515],{},"3.32 bits",[11,5517,5518],{},"The meter reports the calculation for uniform selection from the complete list, while labels such as “Strong” are reference points rather than guarantees. Generation happens locally in the browser, and the tool does not send the resulting phrase to a server. A high-value secret should still be created on a trusted device, kept out of clipboard history, and left unchanged even when one of the random words feels inconvenient; manual improvements can quietly undo the benefit of random selection.",[15,5520,358],{"id":357},[357,5522],{":slug":5523},"[\"passphrase-generator\",\"password-generator\",\"shamir-secret\"]",{"title":363,"searchDepth":364,"depth":364,"links":5525},[5526,5527,5528,5529,5530,5531],{"id":5353,"depth":364,"text":5354},{"id":5415,"depth":364,"text":5416},{"id":5445,"depth":364,"text":5446},{"id":5470,"depth":364,"text":5471},{"id":5486,"depth":364,"text":5487},{"id":357,"depth":364,"text":358},"2026-08-15","The story of Diceware, XKCD #936, and the EFF wordlist: why passphrase strength depends on random selection and how to calculate its entropy correctly.",[5535,5538,5541,5544,5547],{"q":5536,"a":5537},"What is Diceware?","Diceware is a way to create a passphrase with physical dice and a numbered wordlist. Five results form an index from 11111 to 66666, which selects one of 7,776 words; the process is repeated for every additional word.",{"q":5539,"a":5540},"Is every long phrase stronger than a short password?","No. A quotation, song lyric, or sentence invented by a person remains predictable even when it is long. Diceware entropy applies only when every word is selected independently and uniformly at random from a known list.",{"q":5542,"a":5543},"How much entropy does a Diceware passphrase have?","With a list of 7,776 words, one random word contributes about 12.9 bits of entropy. Five independent words provide about 64.6 bits and six provide about 77.5 bits. These figures describe the possible combinations, not a guaranteed cracking time without considering password storage and the attack model.",{"q":5545,"a":5546},"What does XKCD 936 actually teach?","The comic shows that complicated-looking substitutions often follow patterns familiar to attackers, while independently selected words can expand the search space and remain memorable. Its 28-bit and 44-bit figures illustrate particular assumptions rather than providing universal measurements for all passwords.",{"q":5548,"a":5549},"Can this kind of phrase be used as a BIP39 seed phrase?","No. A BIP39 mnemonic encodes machine-generated entropy plus a checksum and uses its own list of 2,048 words. The optional passphrase in BIP39 is a separate input, so an ordinary Diceware phrase cannot replace a wallet recovery mnemonic.","\u002Fimages\u002Fblog\u002Fblog_passphrase-generator.webp",{},"\u002Fblog\u002Fen\u002Fpassphrase-generator",{"title":5336,"description":5533},"blog\u002Fen\u002Fpassphrase-generator",[5556,5557,5558,5559,5560,5561],"What is Diceware","XKCD password comic","Passphrase vs password","Memorable password history","Correct horse battery staple","Passphrase entropy","2IiiXkW-0i8hetBlZfOyvkD8xry11hcxrViKPx9yWLw",{"id":5564,"title":5565,"body":5566,"date":5748,"description":5749,"extension":376,"faq":5750,"image":5766,"lang":394,"meta":5767,"navigation":396,"path":5768,"published":396,"readTime":4389,"seo":5769,"stem":5770,"tags":5771,"updated":405,"__hash__":5778},"blog\u002Fblog\u002Fen\u002Fpassword-generator.md","How Password Research Changed NIST's Guidance",{"type":8,"value":5567,"toc":5739},[5568,5571,5574,5578,5585,5594,5601,5605,5608,5617,5620,5626,5630,5639,5642,5649,5653,5660,5667,5670,5679,5683,5701,5704,5707,5711,5721,5728,5731,5734,5736],[11,5569,5570],{},"Rules such as “eight characters, one uppercase letter, one digit, one symbol, and a mandatory change every 90 days” came from an understandable attempt to make guessing more expensive and shorten the useful life of a stolen secret. The problem was not the arithmetic itself but human behavior. People do not select characters uniformly or invent an entirely new password on schedule, so a formally complex policy often creates patterns that attackers already know.",[11,5572,5573],{},"NIST's guidance did not change because of one sudden discovery. By 2017, research on real breach data, laboratory studies, and observations of password changes had accumulated, and SP 800-63B brought those findings into a more practical security model. The final 2025 revision continued that direction and turned several earlier recommendations into requirements.",[15,5575,5577],{"id":5576},"why-composition-rules-delivered-less-than-promised","Why composition rules delivered less than promised",[11,5579,5580,5581,5584],{},"When a password comes from a uniform generator, expanding its alphabet really does increase the search space. It is easy to turn this true statement into a false assumption that requiring one uppercase letter, digit, and symbol makes a person use the full space with equal probability. In practice, many users capitalize the first letter, place a digit near the end, and choose ",[49,5582,5583],{},"!"," as the symbol.",[11,5586,5587,5588,5593],{},"The study ",[38,5589,5592],{"href":5590,"rel":5591},"https:\u002F\u002Fwww.ece.cmu.edu\u002F~lbauer\u002Fpapers\u002F2011\u002Fchi2011-passwords.pdf",[429],"Of Passwords and People"," compared composition policies not only by their written requirements but also by the passwords people produced, their memorability, and their usability cost. Other work attacked large collections of disclosed passwords and found that conventional policy metrics represented real guess ordering poorly. NIST now summarizes the result directly: composition rules provide less benefit than initially expected and have a severe effect on usability and memorability.",[11,5595,5596,5597,5600],{},"The illustrative password ",[49,5598,5599],{},"Password1!"," satisfies several character categories, but almost all of its structure is predictable. An attacker does not need to begin with an exhaustive search across every printable character. Common words, names, keyboard sequences, and familiar transformations come first. A useful defense must therefore consider not only the size of the permitted alphabet but the probability with which people choose particular values.",[15,5602,5604],{"id":5603},"why-scheduled-rotation-also-lost-its-default-status","Why scheduled rotation also lost its default status",[11,5606,5607],{},"Regular changes had a rational goal. If an attacker quietly obtained a working password, expiration would limit how long it remained useful. That measure only helps under a particular threat model, while every user pays its cost at every rotation. People often preserve the base and change a month, season, number, or final symbol because continually memorizing unrelated secrets is difficult.",[11,5609,5610,5611,5616],{},"The 2010 study ",[38,5612,5615],{"href":5613,"rel":5614},"https:\u002F\u002Ffabianmonrose.github.io\u002Fpapers\u002Fzhang10.pdf",[429],"The Security of Modern Password Expiration"," examined password histories for more than 7,700 accounts and generated guesses for a new password from its predecessor. It found that consecutive values were often connected by predictable transformations, making an old password valuable when searching for the new one. Later user studies refined the picture and did not confirm every feared side effect, but they likewise found no persuasive universal security gain from mandatory rotation.",[11,5618,5619],{},"This does not imply that a password should never be changed. It should be replaced after a leaked hash database, phishing incident, infected device, discovery of password reuse, or other evidence of compromise. NIST moved away specifically from calendar-driven changes without such a signal because they impose recurring work without tying it to a real risk event.",[11,5621,5622],{},[1068,5623],{"alt":5624,"src":5625},"Comparison of legacy password composition and rotation rules with long random password generation","\u002Fimages\u002Fblog\u002Finline_password-generator.webp",[15,5627,5629],{"id":5628},"what-the-2017-edition-actually-said","What the 2017 edition actually said",[11,5631,5632,5633,5638],{},"The first final ",[38,5634,5637],{"href":5635,"rel":5636},"https:\u002F\u002Fpages.nist.gov\u002F800-63-3\u002Fsp800-63b.html",[429],"NIST SP 800-63B"," appeared in 2017 and addressed remote authentication in digital identity systems. That scope matters. The publication defines requirements for the US federal context and serves as an influential industry reference, but it is not a universal law for every website.",[11,5640,5641],{},"For a user-chosen memorized secret, the 2017 edition set a minimum of eight characters and recommended allowing at least 64, including spaces and Unicode. A new or changed value had to be checked against a list of common, expected, and compromised choices. The service also had to limit failed attempts and store the password as a salted hash designed to resist offline attacks, with an adjustable computational cost.",[11,5643,5644,5645,5648],{},"Composition rules and arbitrary periodic changes received ",[49,5646,5647],{},"SHOULD NOT",", a strong recommendation against using them without a compelling reason. The document also recommended allowing paste so that sites would not obstruct password managers. The 2017 approach therefore did more than replace “complexity” with length. It moved attention to the entire defense path, including screening obvious choices, rate limiting, secure storage, and support for unique passwords.",[15,5650,5652],{"id":5651},"what-the-2025-revision-strengthened","What the 2025 revision strengthened",[11,5654,5655,5656,5659],{},"NIST published the final ",[38,5657,5466],{"href":5464,"rel":5658},[429]," in July 2025. A password used as the only authentication factor must now contain at least 15 characters. When it is used only within a multi-factor process, a service may permit a shorter value but no fewer than eight characters. This allowance concerns a centrally verified password within MFA; local device PINs and other activation secrets have a separate section.",[11,5661,5662,5663,5666],{},"The prohibitions on additional composition rules and scheduled password changes now use the normative ",[49,5664,5665],{},"SHALL NOT",". A change is required when there is evidence of compromise. A service must compare the entire prospective password against a blocklist of common, expected, or previously exposed values, reject a match, and explain why. The standard does not require rejecting every dictionary word that happens to appear as a substring.",[11,5668,5669],{},"The 2025 revision also requires support for password managers and autofill, recommends permitting paste, and suggests an option to display the password during entry. Services should allow a maximum length of at least 64 characters, should accept printable ASCII and spaces, and should support Unicode with normalization. NIST also states explicitly that an ordinary password is not phishing-resistant, regardless of how long it is.",[11,5671,5672,5673,5678],{},"A blocklist does not have to send a password to an external service. It can be checked locally. Another option is ",[38,5674,5677],{"href":5675,"rel":5676},"https:\u002F\u002Fhaveibeenpwned.com\u002FAPI\u002Fv3",[429],"Pwned Passwords",", where the client hashes the password, sends only the first five characters of its SHA-1 hash, and compares the full suffix locally. This answers whether the value appears in a known corpus, but a missing match does not prove that the password is strong.",[15,5680,5682],{"id":5681},"where-entropy-is-useful-and-where-it-misleads","Where entropy is useful and where it misleads",[11,5684,5685,5686,5689,5690,5692,5693,5696,5697,5700],{},"For a generator that independently and uniformly selects each of ",[49,5687,5688],{},"L"," characters from a pool of ",[49,5691,1080],{},", the search space contains ",[49,5694,5695],{},"N^L"," possible strings and the entropy is ",[49,5698,5699],{},"L × log₂(N)",". Twelve random lowercase Latin letters provide about 56.4 bits, while eight random characters from 95 printable ASCII symbols provide about 52.6. Within this precisely defined model, additional length can contribute more than several extra character categories.",[11,5702,5703],{},"A password seen in isolation does not carry a reliably measurable amount of “entropy.” Entropy describes the generation process and its probability distribution as known by an attacker. Human choices contain dependent words and symbols, common patterns receive enormous priority, and dictionaries are tried before random enumeration. Universal thresholds such as “80 bits is enough for every case” also conceal the difference between an online login protected by rate limiting and offline guessing against a stolen hash.",[11,5705,5706],{},"Even for a generator, the simple formula may only be an upper bound. Requiring every selected category, forbidding duplicates, fixing the first character, or inserting a user-supplied word changes the distribution. Strength cannot be converted into a cracking time without further assumptions either; speed depends on the storage algorithm, its parameters, the attacker's hardware, and whether guesses can be submitted through the service itself.",[15,5708,5710],{"id":5709},"what-password-generator-does","What Password Generator does",[11,5712,5713,5717,5718,5720],{},[38,5714,5716],{"href":5715},"\u002Ftools\u002Fpassword-generator","Password Generator"," draws the random portion from ",[49,5719,5504],{}," and uses rejection sampling to avoid modulo bias. Length is adjustable from 4 to 128 characters, while the pool can include uppercase and lowercase Latin letters, digits, and symbols. It can remove similar characters, prohibit duplicates, include or exclude specific characters, and require a selected category in the first position.",[11,5722,5723,5724,5727],{},"The meter displays the upper-bound estimate ",[49,5725,5726],{},"log₂(pool size) × random positions",". The tool correctly excludes a fixed inserted word from the random count and warns when too few generated characters remain. Requiring all selected categories, prohibiting repetition, and fixing the beginning still narrow the exact distribution, so the number should be treated as a guide rather than a certificate of strength.",[11,5729,5730],{},"The generator can create a batch of five values and offers three memory views for the current password: four-character chunks, a phonetic reading, and a word sequence for constructing a story. None of these aids is stored, but a password manager remains the better home for a random unique password. All code runs locally in the browser; the value is not transmitted, logged, saved, or checked against breach data.",[11,5732,5733],{},"That final distinction is important. The tool generates a secret but does not implement the complete NIST policy. It permits lengths below the relevant minimum, does not know whether an account uses single-factor authentication or MFA, and contains no blocklist. For an individual account, a practical workflow is to choose a generous length accepted by the service, store a unique result in a manager, and enable phishing-resistant MFA when the service offers it.",[15,5735,358],{"id":357},[357,5737],{":slug":5738},"[\"password-generator\",\"passphrase-generator\",\"key-pair-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":5740},[5741,5742,5743,5744,5745,5746,5747],{"id":5576,"depth":364,"text":5577},{"id":5603,"depth":364,"text":5604},{"id":5628,"depth":364,"text":5629},{"id":5651,"depth":364,"text":5652},{"id":5681,"depth":364,"text":5682},{"id":5709,"depth":364,"text":5710},{"id":357,"depth":364,"text":358},"2026-08-14","Why NIST moved away from composition rules and scheduled password rotation, what changed between the 2017 and 2025 editions, and where entropy is useful.",[5751,5754,5757,5760,5763],{"q":5752,"a":5753},"What did NIST recommend for passwords in 2017?","The first edition of SP 800-63B required at least eight characters for a user-chosen password, screening against common and compromised values, and protection against repeated login attempts. It recommended against additional composition rules and against forcing a password change without evidence of compromise.",{"q":5755,"a":5756},"What changed in NIST SP 800-63B-4 in 2025?","The final revision requires at least 15 characters when a password is the only authentication factor and permits a minimum of eight when the password is used only as part of MFA. The bans on composition rules and periodic changes became normative, while services must now allow password managers and autofill.",{"q":5758,"a":5759},"Why did NIST move away from mandatory uppercase letters, digits, and symbols?","Research and breach analysis showed that people satisfy such rules predictably by capitalizing the first letter, adding a digit and symbol at the end, or modifying a familiar word. This expands the formal alphabet but increases real guessing difficulty much less than expected while imposing substantial usability costs.",{"q":5761,"a":5762},"Should a strong password be changed regularly?","NIST says verifiers must not demand scheduled changes solely because a password has aged, but they must force a change when there is evidence of compromise. A password should also be replaced if it was reused on a breached service, appears in a known leak, or may have been exposed through phishing or malware.",{"q":5764,"a":5765},"Can the strength of every password be measured precisely in entropy bits?","No. The formula L × log₂(N) is exact for a process that independently and uniformly chooses L characters from an alphabet of size N. It does not describe human-created passwords, word-based phrases, or generators with extra constraints. The cost of an attack also depends on guess ordering, rate limits, and password-hash storage.","\u002Fimages\u002Fblog\u002Fblog_password-generator.webp",{},"\u002Fblog\u002Fen\u002Fpassword-generator",{"title":5565,"description":5749},"blog\u002Fen\u002Fpassword-generator",[5772,5773,5774,5775,5776,5777],"NIST password rules","Password entropy","Password requirements outdated","Length over complexity","SP 800-63B","Password manager","fXx3W78rxJ2CjJqo4snWwlQ0mh2D5xhoravphSF4xlE",{"id":5780,"title":5781,"body":5782,"date":6015,"description":6016,"extension":376,"faq":6017,"image":6039,"lang":394,"meta":6040,"navigation":396,"path":6041,"published":396,"readTime":4389,"seo":6042,"stem":6043,"tags":6044,"updated":405,"__hash__":6049},"blog\u002Fblog\u002Fen\u002Fjs-minifier-obfuscator.md","From JSMin to Terser: What Minification Does to JavaScript",{"type":8,"value":5783,"toc":6006},[5784,5787,5791,5800,5803,5813,5817,5826,5829,5840,5844,5854,5870,5883,5887,5910,5926,5929,5935,5939,5942,5945,5961,5965,5984,5990,5998,6001,6003],[11,5785,5786],{},"Readable JavaScript and JavaScript that is efficient to deliver do not have to look alike. Source code benefits from meaningful names, comments, and a structure that supports debugging; the browser only needs a program with the same observable behavior. Minification exploits that difference, but a modern production build does more than remove spaces. It separately decides which code enters the bundle, how the remaining expressions can be shortened, and how the resulting bytes should be compressed in transit.",[15,5788,5790],{"id":5789},"jsmin-removed-only-what-the-interpreter-did-not-need","JSMin removed only what the interpreter did not need",[11,5792,5793,5794,5799],{},"Douglas Crockford released ",[38,5795,5798],{"href":5796,"rel":5797},"https:\u002F\u002Fwww.crockford.com\u002Fjsmin.html",[429],"JSMin"," in 2001 as a small streaming filter for JavaScript. It replaced comments and unnecessary whitespace without building a syntax tree or trying to rewrite the algorithm. JSMin recognized string and regular-expression literals and removed line breaks conservatively because automatic semicolon insertion makes some newlines significant.",[11,5801,5802],{},"That approach suited the early web: a prepared script could pass through a filter immediately before publication. Crockford estimated that a typical file became about half its original size, although the result naturally depended on how much commentary and formatting the source contained. His other qualification matters just as much. JSMin made the output harder to read as a side effect, but he did not describe that as obfuscation.",[11,5804,5805,5806,698,5809,5812],{},"Source reduction already worked alongside transport compression. Crockford recommended applying gzip after JSMin because repeated identifiers and syntax still compress well once whitespace has gone. The distinction remains: a minified file is an executable JavaScript program, while a response sent with ",[49,5807,5808],{},"Content-Encoding: gzip",[49,5810,5811],{},"br"," must first be decoded by the browser.",[15,5814,5816],{"id":5815},"asts-turned-character-removal-into-program-transformation","ASTs turned character removal into program transformation",[11,5818,5819,5820,5825],{},"A streaming filter can inspect neighboring characters but knows little about relationships among expressions, variables, and scopes. In 2010, Mihai Bazon released ",[38,5821,5824],{"href":5822,"rel":5823},"https:\u002F\u002Fgithub.com\u002Fmishoo\u002FUglifyJS-old",[429],"UglifyJS",", a parser, compressor, and beautifier written in JavaScript. By parsing source into an Abstract Syntax Tree, it could make language-level decisions such as shortening local names, folding constant expressions, combining statements, and dropping unreachable branches.",[11,5827,5828],{},"Its phases explain the vocabulary used by current tools. A parser turns text into an AST; a compressor searches for a shorter equivalent form; a mangler renames identifiers where the scope rules permit it; and a printer emits JavaScript with minimal syntactic overhead. UglifyJS2 was an almost complete rewrite of the first version and established this architecture in Node.js build tooling.",[11,5830,510,5831,5836,5837,5839],{},[38,5832,5835],{"href":5833,"rel":5834},"https:\u002F\u002Fdevelopers.google.com\u002Fclosure\u002Fcompiler\u002Fdocs\u002Fcompilation_levels",[429],"Closure Compiler's compilation levels"," illustrate the same progression. Its simple mode removes whitespace and shortens local names, while advanced mode analyzes the program globally, removes unreachable code, and performs inlining. Aggressive analysis comes with stronger assumptions: external names, dynamic ",[49,5838,1709],{},", or an interface between compiled and uncompiled code can make a transformation unsafe. A minifier does not understand the program without limits; it proves particular rewrites inside a defined model.",[15,5841,5843],{"id":5842},"new-syntax-required-a-parser-that-kept-moving","New syntax required a parser that kept moving",[11,5845,5846,5847,1121,5850,5853],{},"Before ECMAScript 2015, much production output was transpiled into ES5 syntax. Arrow functions, ",[49,5848,5849],{},"let",[49,5851,5852],{},"const",", classes, destructuring, template literals, modules, and constructs from later annual editions then became ordinary JavaScript. A minifier cannot safely rewrite an expression its parser cannot recognize, so language support became continuous maintenance rather than a problem solved once.",[11,5855,5856,5857,5860,5861,5866,5867,5869],{},"UglifyJS continued its ES5-oriented line, while ",[49,5858,5859],{},"uglify-es"," handled newer syntax for a time. When maintenance of that project stopped, Terser appeared in 2018. Its ",[38,5862,5865],{"href":5863,"rel":5864},"https:\u002F\u002Fgithub.com\u002Fterser\u002Fterser",[429],"project documentation"," describes it as a fork of ",[49,5868,5859],{}," that retained substantial API and CLI compatibility with UglifyJS 3 while targeting ES6 and newer JavaScript. The fork was not the result of one dramatic ES6 bug; it addressed the recurring need to maintain a changing grammar and the optimizations built on top of it.",[11,5871,5872,5873,673,5876,1228,5879,5882],{},"Terser retained the familiar ",[49,5874,5875],{},"compress",[49,5877,5878],{},"mangle",[49,5880,5881],{},"format"," stages. It can account for module semantics, preserve function and class names, emit source maps, and constrain risky transformations through options. Its place in current tooling reflects the way minification became one stage in a bundler pipeline, after source code had already passed through module resolution, transpilation, and other build steps.",[15,5884,5886],{"id":5885},"a-production-pipeline-reduces-delivery-in-several-different-ways","A production pipeline reduces delivery in several different ways",[11,5888,5889,5890,1121,5893,5895,5896,5901,5902,5905,5906,5909],{},"Tree shaking runs before minification and reasons about a module graph. Static ",[49,5891,5892],{},"import",[49,5894,3790],{}," declarations let a bundler identify used exports, but side effects complicate the decision. A CSS import, event-handler registration, or global mutation cannot be discarded simply because no variable receives the import result. The ",[38,5897,5900],{"href":5898,"rel":5899},"https:\u002F\u002Fwebpack.js.org\u002Fguides\u002Ftree-shaking\u002F",[429],"webpack tree-shaking guide"," therefore distinguishes ",[49,5903,5904],{},"usedExports"," analysis from ",[49,5907,5908],{},"sideEffects"," declarations before handing the remaining code to a minifier.",[11,5911,5912,5913,5918,5919,1121,5922,5925],{},"Terser can then remove local dead code, simplify expressions, and shorten names. This is not project-wide tree shaking because a standalone minifier neither resolves dependencies nor knows the complete application graph. At the final network stage, the server applies gzip or Brotli. ",[38,5914,5917],{"href":5915,"rel":5916},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FHTTP\u002FGuides\u002FCompression",[429],"MDN's HTTP compression guide"," explains how browsers and servers negotiate the encoding through ",[49,5920,5921],{},"Accept-Encoding",[49,5923,5924],{},"Content-Encoding","; the process reduces transmitted bytes without modifying the repository source.",[11,5927,5928],{},"These optimizations accumulate rather than duplicate one another. Tree shaking excludes unnecessary modules, minification shrinks the JavaScript that remains, and gzip or Brotli exploits repetition in the resulting text. A source map serves the opposite operational need by mapping a failure in the compact bundle back to an original file. Teams should make a deliberate decision about publishing maps with embedded source content or storing them separately, but minification should not make production failures impossible to investigate.",[11,5930,5931],{},[1068,5932],{"alt":5933,"src":5934},"The upper lane separates tree shaking, Terser minification, and HTTP compression, while the lower lane shows the added complexity and possible size growth of obfuscation","\u002Fimages\u002Fblog\u002Finline_js-minifier-obfuscator.webp",[15,5936,5938],{"id":5937},"obfuscation-optimizes-the-cost-of-analysis-not-file-size","Obfuscation optimizes the cost of analysis, not file size",[11,5940,5941],{},"An obfuscator changes the objective. Identifier renaming, string arrays, literal splitting, and control-flow flattening are meant to obscure the program's intent even when the output becomes larger and slower. Dead-code injection deliberately adds instructions that execution does not need, putting it in direct conflict with minimum size.",[11,5943,5944],{},"Encoding strings with Base64 or RC4 does not turn a client bundle into a secret store. The decoder and required data must accompany the program so that it can recover the value at runtime. An analyst can stop execution after decoding, inspect a network request, or observe values in memory. A license decision, permanent private API key, or authorization check therefore cannot be entrusted solely to browser code.",[11,5946,5947,5948,5953,5954,1121,5957,5960],{},"Obfuscation can still help when the objective is narrower: remove obvious names, discourage casual copying of a small proprietary routine, or increase the time required for manual analysis. It remains a tradeoff. The ",[38,5949,5952],{"href":5950,"rel":5951},"https:\u002F\u002Fgithub.com\u002Fjavascript-obfuscator\u002Fjavascript-obfuscator",[429],"JavaScript Obfuscator documentation"," warns that control-flow flattening slows execution and increases code size, while dead-code injection can expand it especially sharply. ",[49,5955,5956],{},"selfDefending",[49,5958,5959],{},"debugProtection"," also interfere with formatting and debugging, making work harder for the owner's team as well as an outside analyst.",[15,5962,5964],{"id":5963},"what-the-tool-actually-does","What the tool actually does",[11,5966,510,5967,5971,5972,1121,5974,5976,5977,673,5980,5983],{},[38,5968,5970],{"href":5969},"\u002Ftools\u002Fjs-minifier-obfuscator","JS Minifier & Obfuscator"," loads bundled browser builds of Terser and JavaScript Obfuscator, then performs every transformation in the current tab without uploading the source. Minify mode separates ",[49,5973,5875],{},[49,5975,5878],{},", offers Safe, Default, and Aggressive presets, and exposes controls for unreachable-code removal, ",[49,5978,5979],{},"console.*",[49,5981,5982],{},"debugger",", function and class names, top-level mangling, and module semantics.",[11,5985,5986,5987,5989],{},"The Safe label describes a more conservative set of options, not a proof that the output is safe. Aggressive mode may remove ",[49,5988,5979],{}," and rename top-level symbols, so an enclosing page, test, or separate script can lose a reference it expects. The tool neither executes the output nor creates a source map; copied or downloaded code still needs validation in its target environment.",[11,5991,5992,5993,1228,5995,5997],{},"Obfuscate mode provides four presets and individual controls for string arrays, Base64 or RC4 encoding, string splitting, control-flow flattening, dead-code injection, number expressions, global identifier renaming, ",[49,5994,5956],{},[49,5996,5959],{},". The interface warns that High increases output size and runtime cost. Its displayed comparison measures raw UTF-8 bytes before and after transformation, not gzip or Brotli transfer size.",[11,5999,6000],{},"This is a tool for one supplied JavaScript fragment. The Module flag tells Terser how to interpret that input, but it does not turn the page into a bundler: imports are not resolved, no dependency graph is built, and project tree shaking does not occur. The experiment can help select parameters, while the final production transformation should remain a reproducible pipeline stage alongside tests, source maps, and HTTP compression.",[15,6002,358],{"id":357},[357,6004],{":slug":6005},"[\"js-minifier-obfuscator\",\"html-css-beautifier\",\"sql-formatter\"]",{"title":363,"searchDepth":364,"depth":364,"links":6007},[6008,6009,6010,6011,6012,6013,6014],{"id":5789,"depth":364,"text":5790},{"id":5815,"depth":364,"text":5816},{"id":5842,"depth":364,"text":5843},{"id":5885,"depth":364,"text":5886},{"id":5937,"depth":364,"text":5938},{"id":5963,"depth":364,"text":5964},{"id":357,"depth":364,"text":358},"2026-08-12","How minification moved from whitespace removal to AST analysis, why Terser appeared, and how it differs from tree shaking and obfuscation.",[6018,6021,6024,6027,6030,6033,6036],{"q":6019,"a":6020},"Who created JSMin, and what did it do?","Douglas Crockford released JSMin in 2001. The streaming filter removed comments and unnecessary whitespace while conservatively preserving line breaks that JavaScript might interpret as statement boundaries. JSMin did not build an AST or rename variables, and it was explicitly distinguished from an obfuscator.",{"q":6022,"a":6023},"How is Terser different from UglifyJS?","Mihai Bazon released UglifyJS in 2010 as a JavaScript parser, compressor, and mangler written in JavaScript. As ECMAScript syntax moved well beyond ES5, the uglify-es branch appeared but later stopped being maintained. Terser began in 2018 as a compatible fork of uglify-es for modern JavaScript and continued its AST transformation model.",{"q":6025,"a":6026},"How do minify, compress, and mangle differ?","Minify describes the overall goal of making the program text smaller. In Terser terminology, compress rewrites expressions and removes code it can prove unnecessary, mangle shortens eligible identifier names, and format prints the resulting AST with minimal syntax. Each stage must preserve observable behavior under its documented assumptions.",{"q":6028,"a":6029},"Are tree shaking and minification the same process?","No. Tree shaking analyzes an ES module graph and excludes unused exports or entire modules while accounting for side effects. A minifier works on the code that remains, although it may then remove additional local expressions and unreachable branches. The two stages complement each other.",{"q":6031,"a":6032},"Does minification replace gzip or Brotli?","No. Minification creates a smaller JavaScript file that the browser can execute directly. gzip and Brotli encode bytes for HTTP transfer, and the browser restores the content before parsing it. A production pipeline normally bundles and minifies first, then serves the result with gzip or Brotli.",{"q":6034,"a":6035},"Can obfuscation hide an API key or password?","A secret cannot be reliably hidden in client-side JavaScript. The browser needs the code, decoder, and data required to run the program, so the user can inspect them as well. Obfuscation may slow down analysis, but permanent keys, trusted checks, and privileged operations belong on a server.",{"q":6037,"a":6038},"Can minification or obfuscation break a program?","Yes, when a transformation violates an implicit contract. Dynamic eval, Function.name access, external references to global names, serialized functions, and code with significant side effects all require care. Transformed output needs the same tests used for any other production build.","\u002Fimages\u002Fblog\u002Fblog_js-minifier-obfuscator.webp",{},"\u002Fblog\u002Fen\u002Fjs-minifier-obfuscator",{"title":5781,"description":6016},"blog\u002Fen\u002Fjs-minifier-obfuscator",[6045,5798,5824,6046,6047,6048],"JavaScript minification history","Terser","Tree shaking","JavaScript obfuscation","bC3sn46gBBnj6mbPRFLgrIMZwqyOnrrAngTxOmlhDLM",{"id":6051,"title":6052,"body":6053,"date":6636,"description":6637,"extension":376,"faq":6638,"image":6672,"lang":394,"meta":6673,"navigation":396,"path":6674,"published":396,"readTime":742,"seo":6675,"stem":6676,"tags":6677,"updated":405,"__hash__":6686},"blog\u002Fblog\u002Fen\u002Fog-image-generator.md","OG Image Generator: Visual Editor for Open Graph Cards",{"type":8,"value":6054,"toc":6621},[6055,6058,6061,6065,6072,6075,6098,6102,6105,6108,6144,6148,6151,6201,6204,6208,6211,6214,6217,6220,6224,6227,6244,6251,6255,6262,6265,6288,6291,6294,6397,6414,6418,6421,6431,6434,6454,6457,6461,6464,6478,6484,6488,6491,6494,6497,6523,6526,6530,6544,6547,6561,6565,6568,6571,6575,6609,6612,6615,6618],[11,6056,6057],{},"The Open Graph image is the only part of a page most people see before deciding whether to click. It shows up in group chats, Slack channels, LinkedIn feeds and X timelines, and in every one of those places it is competing with everything else on screen.",[11,6059,6060],{},"The specification is loose. The practical constraints are not.",[15,6062,6064],{"id":6063},"what-the-tool-does","What the tool does",[11,6066,510,6067,6071],{},[38,6068,6070],{"href":6069},"\u002Ftools\u002Fog-image-generator","OG Image Generator"," began as a 1200×630 image generator, but it has grown into a compact visual editor. There is no single rigid template where only the headline and two colours can change. You build a composition from independent elements and edit the result exactly where you see it.",[11,6073,6074],{},"The editor covers the complete path from an idea to a publishable file:",[298,6076,6077,6080,6083,6086,6089,6092,6095],{},[301,6078,6079],{},"create Open Graph, Instagram, Story, cover and custom-size images;",[301,6081,6082],{},"move, resize and rotate elements directly on the canvas;",[301,6084,6085],{},"keep several translations in one composition, including LTR and RTL;",[301,6087,6088],{},"store named cards and the current draft locally in the browser;",[301,6090,6091],{},"move one template or the complete library through JSON;",[301,6093,6094],{},"export PNG, JPEG and WebP at 1× or 2×;",[301,6096,6097],{},"generate meta tags and preview the resulting social card.",[15,6099,6101],{"id":6100},"why-this-is-a-visual-editor-rather-than-a-settings-form","Why this is a visual editor rather than a settings form",[11,6103,6104],{},"Most work happens on the canvas. Select an object and drag it with a mouse or pointer, pull its handles to resize it, use the separate rotation control, and double-click text to edit it in place. Exact coordinates, width, height, opacity and rotation remain available in the inspector, but those controls support direct manipulation instead of replacing it.",[11,6106,6107],{},"The editor includes the composition tools expected from a small design application:",[298,6109,6110,6116,6122,6128,6138],{},[301,6111,6112,6115],{},[26,6113,6114],{},"Snapping and guides."," Objects snap to the canvas edges and centre as well as the boundaries of neighbouring elements. A text box can therefore match an image width precisely.",[301,6117,6118,6121],{},[26,6119,6120],{},"Alignment and distribution."," Multiple selected objects can share a left, right or centre edge and be distributed with equal gaps.",[301,6123,6124,6127],{},[26,6125,6126],{},"Layers."," Raise or lower an object, hide it temporarily, or lock it against accidental movement.",[301,6129,6130,6133,6134,6137],{},[26,6131,6132],{},"Precise nudging."," Arrow keys move a selection by one pixel and ",[49,6135,6136],{},"Shift"," moves it by ten. Undo and Redo make experimentation safe.",[301,6139,6140,6143],{},[26,6141,6142],{},"Safe area and Fit zoom."," The safe area keeps important copy away from risky edges, while Fit keeps the whole composition visible regardless of window size.",[15,6145,6147],{"id":6146},"the-building-blocks","The building blocks",[11,6149,6150],{},"Instead of hundreds of presets, the editor provides a small set of reusable elements:",[298,6152,6153,6159,6165,6171,6177,6183,6189,6195],{},[301,6154,6155,6158],{},[26,6156,6157],{},"Text"," for headlines, subtitles, site addresses and labels, with optional auto-fit;",[301,6160,6161,6164],{},[26,6162,6163],{},"Image and Logo"," for photos, illustrations and brand marks in PNG, JPEG, WebP or SVG;",[301,6166,6167,6170],{},[26,6168,6169],{},"Icon"," for a standalone built-in or uploaded icon;",[301,6172,6173,6176],{},[26,6174,6175],{},"Icon + text"," for a phone number, address, link or short benefit;",[301,6178,6179,6182],{},[26,6180,6181],{},"Icon list"," for repeatable contacts, services or benefits with drag-and-drop ordering, dividers and configurable icon placement;",[301,6184,6185,6188],{},[26,6186,6187],{},"Panel"," for a solid, gradient or textured region that divides the composition;",[301,6190,6191,6194],{},[26,6192,6193],{},"Shape"," for lines, separators and decoration;",[301,6196,6197,6200],{},[26,6198,6199],{},"Button"," for a CTA or visual link.",[11,6202,6203],{},"That is enough for a simple “logo — headline — subtitle” card and for a split layout with a photo, contact information and a row of benefits. Every block stays independent: duplicate it, attach it to a panel, change its layer order or remove it.",[15,6205,6207],{"id":6206},"colour-backgrounds-images-and-typography","Colour, backgrounds, images and typography",[11,6209,6210],{},"Each document has a role-based palette for the background, panel, accent, primary text and muted text. Elements that use a role update together when the palette changes, while any colour can still be made local when a one-off value is needed.",[11,6212,6213],{},"The canvas and panel backgrounds support solid and gradient fills. Both gradient stops have independent opacity, so a transition can run from transparent into a colour. Dot and grid textures are available, while a background photo adds scale, position, tint colour and tint strength controls.",[11,6215,6216],{},"Uploaded images can be flipped horizontally or vertically, faded and rotated through a continuous 0–359° range. Text controls include size, auto-fit minimum size, weight, alignment, letter spacing, colour and a shadow with independent X\u002FY offsets, blur and opacity.",[11,6218,6219],{},"The font library covers Latin, Cyrillic and Hebrew with Manrope, Inter, Montserrat, Oswald, Lora, Merriweather, Playfair Display, JetBrains Mono, Rubik, Heebo, Assistant, Noto Sans Hebrew, Frank Ruhl Libre and more. A font can be the default for a language or an override for one specific element.",[15,6221,6223],{"id":6222},"one-composition-for-english-russian-and-hebrew","One composition for English, Russian and Hebrew",[11,6225,6226],{},"Translation should not require manually rebuilding the layout. An OG Image Generator card keeps one element structure with several text versions. Arrange the composition once, switch a language chip and edit only the copy.",[11,6228,6229,6230,673,6233,6236,6237,698,6240,6243],{},"Each locale can use explicit ",[49,6231,6232],{},"LTR",[49,6234,6235],{},"RTL"," or automatic direction and an appropriate default font. For Hebrew, direction changes both on the rendered canvas and inside the editing ",[49,6238,6239],{},"input",[49,6241,6242],{},"textarea",", so the string starts from the correct side. Icon lists can detect direction per item while keeping icons physically left, right or tied to the text direction.",[11,6245,6246,6247,6250],{},"When a new locale is added, copy from an existing one by filling only empty fields or deliberately overwriting everything after confirmation. During export, ",[26,6248,6249],{},"Export all languages"," creates one ready image per locale with shared geometry and the correct copy.",[15,6252,6254],{"id":6253},"the-dimensions-that-matter","The dimensions that matter",[11,6256,6257,6258,6261],{},"For Open Graph, ",[26,6259,6260],{},"1200×630"," at a 1.91:1 ratio remains the primary target. Every major platform crops from it, which is why it has become the de facto standard rather than an official one. Below roughly 600×315, platforms may refuse to render a large card and fall back to a small thumbnail.",[11,6263,6264],{},"The same editor is useful beyond OG images. Its presets include:",[298,6266,6267,6270,6273,6276,6279,6282,6285],{},[301,6268,6269],{},"Instagram Square — 1080×1080;",[301,6271,6272],{},"Instagram Portrait — 1080×1350;",[301,6274,6275],{},"Instagram Landscape — 1080×566;",[301,6277,6278],{},"Story — 1080×1920;",[301,6280,6281],{},"Profile cover — 1500×500;",[301,6283,6284],{},"Wide — 1920×1080;",[301,6286,6287],{},"Custom — any size from 240 to 4096 pixels on either axis.",[11,6289,6290],{},"When dimensions change, either resize the canvas while leaving elements alone or scale the whole composition. A normal canvas-only resize does not rewrite font sizes: typography remains an intentional setting rather than an accidental side effect of changing format.",[11,6292,6293],{},"Declare the size in your markup as well:",[1564,6295,6299],{"className":6296,"code":6297,"language":6298,"meta":363,"style":363},"language-html shiki shiki-themes github-dark","\u003Cmeta property=\"og:image\" content=\"https:\u002F\u002Fexample.com\u002Fog\u002Fpost.png\">\n\u003Cmeta property=\"og:image:width\" content=\"1200\">\n\u003Cmeta property=\"og:image:height\" content=\"630\">\n\u003Cmeta name=\"twitter:card\" content=\"summary_large_image\">\n","html",[49,6300,6301,6330,6352,6374],{"__ignoreMap":363},[1572,6302,6303,6306,6309,6313,6316,6319,6322,6324,6327],{"class":1574,"line":1575},[1572,6304,6305],{"class":1638},"\u003C",[1572,6307,6308],{"class":3666},"meta",[1572,6310,6312],{"class":6311},"svObZ"," property",[1572,6314,6315],{"class":1638},"=",[1572,6317,6318],{"class":1651},"\"og:image\"",[1572,6320,6321],{"class":6311}," content",[1572,6323,6315],{"class":1638},[1572,6325,6326],{"class":1651},"\"https:\u002F\u002Fexample.com\u002Fog\u002Fpost.png\"",[1572,6328,6329],{"class":1638},">\n",[1572,6331,6332,6334,6336,6338,6340,6343,6345,6347,6350],{"class":1574,"line":364},[1572,6333,6305],{"class":1638},[1572,6335,6308],{"class":3666},[1572,6337,6312],{"class":6311},[1572,6339,6315],{"class":1638},[1572,6341,6342],{"class":1651},"\"og:image:width\"",[1572,6344,6321],{"class":6311},[1572,6346,6315],{"class":1638},[1572,6348,6349],{"class":1651},"\"1200\"",[1572,6351,6329],{"class":1638},[1572,6353,6354,6356,6358,6360,6362,6365,6367,6369,6372],{"class":1574,"line":370},[1572,6355,6305],{"class":1638},[1572,6357,6308],{"class":3666},[1572,6359,6312],{"class":6311},[1572,6361,6315],{"class":1638},[1572,6363,6364],{"class":1651},"\"og:image:height\"",[1572,6366,6321],{"class":6311},[1572,6368,6315],{"class":1638},[1572,6370,6371],{"class":1651},"\"630\"",[1572,6373,6329],{"class":1638},[1572,6375,6376,6378,6380,6383,6385,6388,6390,6392,6395],{"class":1574,"line":1591},[1572,6377,6305],{"class":1638},[1572,6379,6308],{"class":3666},[1572,6381,6382],{"class":6311}," name",[1572,6384,6315],{"class":1638},[1572,6386,6387],{"class":1651},"\"twitter:card\"",[1572,6389,6321],{"class":6311},[1572,6391,6315],{"class":1638},[1572,6393,6394],{"class":1651},"\"summary_large_image\"",[1572,6396,6329],{"class":1638},[11,6398,6399,1121,6402,6405,6406,6409,6410,6413],{},[49,6400,6401],{},"og:image:width",[49,6403,6404],{},"og:image:height"," let the platform reserve layout space before the image finishes downloading. Without them the card can render in a degraded form on first paint. And without ",[49,6407,6408],{},"twitter:card"," set to ",[49,6411,6412],{},"summary_large_image",", X shows the small square card no matter how good your image is.",[15,6415,6417],{"id":6416},"repeatability-templates-the-local-library-and-json","Repeatability: templates, the local library and JSON",[11,6419,6420],{},"The expensive part is not replacing a headline. It is designing a good system once: spacing, palette, type, logo placement, grid and translation rules. A finished card can therefore be saved to the local library and reused as the starting point for future publications.",[11,6422,6423,6426,6427,6430],{},[26,6424,6425],{},"Save"," updates the current card, while ",[26,6428,6429],{},"Save as new"," creates an independent copy under a new name. The active draft is autosaved. When another template or a blank document would replace unsaved work, the editor offers to save first, continue without saving or cancel the action.",[11,6432,6433],{},"The library remains in the browser. JSON provides backup and handoff:",[298,6435,6436,6442,6448],{},[301,6437,6438,6441],{},[26,6439,6440],{},"Export JSON"," saves the current card with its elements, translations, palette and uploaded assets;",[301,6443,6444,6447],{},[26,6445,6446],{},"Export all as JSON"," downloads the whole library;",[301,6449,6450,6453],{},[26,6451,6452],{},"Import"," adds documents as new records and validates their structure before loading them.",[11,6455,6456],{},"This makes it practical to build one branded master template, send it to a translator or colleague, and receive the same composition back without manually recreating coordinates.",[15,6458,6460],{"id":6459},"image-and-meta-tag-export","Image and meta-tag export",[11,6462,6463],{},"The export modal offers PNG, JPEG or WebP at 1×\u002F2×. Download the active locale, copy the rendered image to the clipboard, or export every locale in one action. Filenames include the card name, locale and final dimensions so variants do not overwrite one another accidentally.",[11,6465,6466,6467,6470,6471,1121,6474,6477],{},"The separate ",[26,6468,6469],{},"Meta tags & social preview"," modal connects the design to a page. Choose which text element supplies ",[49,6472,6473],{},"og:title",[49,6475,6476],{},"og:description",", enter the page URL and future absolute image URL, inspect the social preview, and copy the complete Open Graph\u002FTwitter markup.",[11,6479,6480,6481,507],{},"The editor creates the file but does not upload it to a host. After export, the image still needs a public HTTPS URL and the generated markup must be placed in the page ",[49,6482,6483],{},"\u003Chead>",[15,6485,6487],{"id":6486},"design-for-the-thumbnail-not-the-mockup","Design for the thumbnail, not the mockup",[11,6489,6490],{},"This is where most generated cards fail, and it is a design problem rather than a technical one.",[11,6492,6493],{},"A card looks great at full width on your monitor. In a phone chat window it renders at maybe a third of that. A 40-word title set in a light weight becomes grey mush; a subtitle in 60% opacity disappears entirely.",[11,6495,6496],{},"Practical rules that survive the shrink:",[298,6498,6499,6505,6511,6517],{},[301,6500,6501,6504],{},[26,6502,6503],{},"Titles under ~70 characters."," Past that the auto-fit sizing has to shrink the type to keep it on the box, and legibility collapses. If a headline keeps shrinking as you type, that is the signal to cut it.",[301,6506,6507,6510],{},[26,6508,6509],{},"Weight over size."," A bold 60px title reads better small than a light 80px one.",[301,6512,6513,6516],{},[26,6514,6515],{},"High contrast."," Mid-grey text on a mid-grey gradient is invisible at thumbnail scale.",[301,6518,6519,6522],{},[26,6520,6521],{},"One idea."," The card is not a slide. Title, one supporting line, brand — anything more is noise.",[11,6524,6525],{},"Zoom out to 25–50% in the editor before exporting — that is roughly the size a card actually renders at in a phone chat window, and it is a much faster way to catch an illegible title than guessing from the full-size canvas.",[15,6527,6529],{"id":6528},"absolute-urls-always","Absolute URLs, always",[11,6531,6532,6533,6536,6537,6540,6541,507],{},"Crawlers ignore relative paths. ",[49,6534,6535],{},"\u002Fog\u002Fpost.png"," will silently produce no image; it must be ",[49,6538,6539],{},"https:\u002F\u002Fexample.com\u002Fog\u002Fpost.png",". The same applies to ",[49,6542,6543],{},"og:url",[11,6545,6546],{},"Two related failure modes worth checking if an image refuses to appear:",[298,6548,6549,6555],{},[301,6550,6551,6554],{},[26,6552,6553],{},"The image is behind a redirect or auth."," The URL must return the image bytes directly to an unauthenticated request. A 302 to a CDN sometimes works and sometimes does not, depending on the crawler.",[301,6556,6557,6560],{},[26,6558,6559],{},"The file is too large."," Keep it comfortably under a few megabytes. A 1200×630 PNG of flat colours and text is typically well under 200 KB, which is one reason canvas-rendered cards beat screenshots.",[15,6562,6564],{"id":6563},"unfurl-caches-are-aggressive","Unfurl caches are aggressive",[11,6566,6567],{},"Once a platform has scraped your URL, it caches the result — often for days. Replacing the image at the same URL will not update existing previews, and frequently will not update new ones either.",[11,6569,6570],{},"Two ways out: use each platform's own debugger to force a re-scrape, or publish the new image under a different filename and update the meta tag. The second is more reliable and works everywhere at once, which is why versioned filenames are a good habit for pages whose cards change.",[15,6572,6574],{"id":6573},"workflow","Workflow",[4001,6576,6577,6580,6583,6586,6589,6592,6595,6598,6601,6606],{},[301,6578,6579],{},"Choose the target size: 1200×630 for Open Graph, a social preset or Custom.",[301,6581,6582],{},"Start from a nearby template or a blank canvas, then establish the palette, background and primary panels.",[301,6584,6585],{},"Add the logo, image, headline, subtitle and any contact or supporting blocks.",[301,6587,6588],{},"Build the composition with Snap, guides, alignment and layers; lock finished background elements.",[301,6590,6591],{},"Write the real headline rather than a placeholder because length affects auto-fit and wrapping.",[301,6593,6594],{},"Add locales, choose direction and fonts, then replace the copy in each language.",[301,6596,6597],{},"Zoom out to 25–50% and confirm the card remains readable as a small preview.",[301,6599,6600],{},"Save the result to the library or export JSON as a portable branded template.",[301,6602,6603,6604,507],{},"Download PNG\u002FJPEG\u002FWebP, publish it at an absolute HTTPS URL and copy the meta tags into ",[49,6605,6483],{},[301,6607,6608],{},"If it replaces an image at the same address, trigger a re-scrape in the relevant platform debugger.",[11,6610,6611],{},"Text is drawn with the Canvas 2D API. The required webfonts are loaded before export, so the final file matches what the canvas showed rather than silently falling back to a system font. The document, uploaded images and export all stay local: no account or server upload of source assets is required.",[15,6613,6614],{"id":357},"Related Tools",[357,6616],{":slug":6617},"[\"og-image-generator\",\"seo-geo-studio\",\"color-toolbox\",\"image-optimizer\"]",[1779,6619,6620],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":6622},[6623,6624,6625,6626,6627,6628,6629,6630,6631,6632,6633,6634,6635],{"id":6063,"depth":364,"text":6064},{"id":6100,"depth":364,"text":6101},{"id":6146,"depth":364,"text":6147},{"id":6206,"depth":364,"text":6207},{"id":6222,"depth":364,"text":6223},{"id":6253,"depth":364,"text":6254},{"id":6416,"depth":364,"text":6417},{"id":6459,"depth":364,"text":6460},{"id":6486,"depth":364,"text":6487},{"id":6528,"depth":364,"text":6529},{"id":6563,"depth":364,"text":6564},{"id":6573,"depth":364,"text":6574},{"id":357,"depth":364,"text":6614},"2026-08-11","Build Open Graph cards visually with direct canvas editing, multilingual RTL text, reusable templates, local storage, image export and ready meta tags.",[6639,6642,6645,6648,6651,6654,6657,6660,6663,6666,6669],{"q":6640,"a":6641},"What size should an Open Graph image be?","1200×630 pixels, a 1.91:1 ratio. That is what X, LinkedIn, Facebook and Slack all crop from. Going larger wastes bytes and going smaller gets upscaled and looks soft.",{"q":6643,"a":6644},"Can one card contain several language versions?","Yes. Text for every locale lives inside one composition, so element placement stays shared. The editor changes text and input direction for Hebrew and other RTL languages, and export can download every language version at once.",{"q":6646,"a":6647},"Which elements can I add to a card?","Text, images, logos, standalone icons, icon-and-text rows, icon lists, panels, shapes and buttons. Elements can be dragged, resized, rotated, aligned, hidden, locked and reordered in the layer stack.",{"q":6649,"a":6650},"Can I upload an SVG logo or icon?","Yes. The editor accepts SVG as well as PNG, JPEG and WebP for images, logos, canvas backgrounds and custom icons.",{"q":6652,"a":6653},"How do I move a template to another browser?","Export the current card or the whole local library as JSON, then import that file in another browser. The JSON preserves structure, settings, translations and embedded assets.",{"q":6655,"a":6656},"Why does my OG image not show up when I share the link?","The most common causes are a relative image URL, an image behind authentication or a redirect, and a stale unfurl cache. Crawlers require an absolute HTTPS URL that returns the image directly.",{"q":6658,"a":6659},"Do I need both og:image and twitter:image?","Not strictly — X falls back to og:image if twitter:image is absent. But you do need twitter:card set to summary_large_image, otherwise you get the small square card instead of the wide one.",{"q":6661,"a":6662},"Why does my updated image still show the old version?","Unfurl results are cached aggressively. Use the platform's own debugger to force a re-scrape, or publish the new image at a different filename.",{"q":6664,"a":6665},"How long should the title on an OG image be?","Under about 70 characters. The image often renders at a third of its size in a phone chat window, so long titles set in light weights turn into unreadable grey blocks — zoom the editor out to 25–50% to check before exporting.",{"q":6667,"a":6668},"Should og:image:width and og:image:height be declared?","Yes. They let the platform reserve layout space before the image downloads, which avoids a reflow and makes the card render correctly on the first pass.",{"q":6670,"a":6671},"Is the image uploaded anywhere to be generated?","No. The card is drawn with the Canvas 2D API and PNG, JPEG or WebP output is produced locally, so nothing is sent to a server.","\u002Fimages\u002Fblog\u002Fblog_og-image-generator.webp",{},"\u002Fblog\u002Fen\u002Fog-image-generator",{"title":6052,"description":6637},"blog\u002Fen\u002Fog-image-generator",[6678,6679,6680,6681,6682,6683,6684,6685],"open graph image","og image generator","twitter card","social share image","link preview","seo meta tags","visual editor","card templates","mRDp1qPYZAKm_0PLcnrBRFaNC-gr-iPIXAWX3o6NKAc",{"id":6688,"title":6689,"body":6690,"date":6887,"description":6888,"extension":376,"faq":6889,"image":6902,"lang":394,"meta":6903,"navigation":396,"path":6904,"published":396,"readTime":1270,"seo":6905,"stem":6906,"tags":6907,"updated":405,"__hash__":6914},"blog\u002Fblog\u002Fen\u002Fstring-transformer.md","Where Mojibake Comes From in Cyrillic Text",{"type":8,"value":6691,"toc":6878},[6692,6702,6705,6709,6716,6730,6737,6743,6747,6762,6765,6776,6779,6783,6792,6795,6798,6802,6809,6812,6816,6830,6839,6842,6846,6853,6860,6863,6873,6875],[11,6693,6694,6695,698,6698,6701],{},"Anyone who used the Russian-language internet in the 1990s or early 2000s is likely to remember an email, webpage, or text file in which ordinary words appeared as ",[49,6696,6697],{},"РџСЂРёРІРµС‚",[49,6699,6700],{},"╨Я╤А╨╕╨▓╨╡╤В",". Russian speakers often call such text krakozyabry, while international technical writing commonly uses the Japanese term mojibake (文字化け).",[11,6703,6704],{},"These strings are not random, and they usually do not indicate a hardware failure. The bytes of the original message may still be present, but the program has read them through the wrong character table. Cyrillic made the problem particularly visible because DOS, Unix, Windows, and early network services used different single-byte encodings for years, while the information identifying the intended table could be lost or mislabeled in transit.",[15,6706,6708],{"id":6707},"bytes-do-not-know-which-letters-they-represent","Bytes do not know which letters they represent",[11,6710,6711,6712,6715],{},"A text file stores numbers rather than letters. An encoding is the agreement that maps each number, or sequence of numbers, to a character. In a legacy single-byte encoding, one byte usually represents one character, so a value such as ",[49,6713,6714],{},"0xCF"," can mean different letters in different tables. UTF-8 works differently because characters outside ASCII are represented by multibyte sequences.",[11,6717,6718,6719,6722,6723,6726,6727,6729],{},"The Russian word ",[49,6720,6721],{},"Привет"," begins with the bytes ",[49,6724,6725],{},"D0 9F D1 80"," in UTF-8. Give the complete sequence to a UTF-8 decoder and it produces the original word. Interpret those same bytes as Windows-1251 and the result is ",[49,6728,6697],{},"; interpret them as CP866 and a different garbled string appears. The bytes have not changed. Only the decoding rule has.",[11,6731,6732,6733,6736],{},"This also explains a crucial limit of encoding repair. Mojibake can be reversed only while the mistake itself remains reversible. If an editor replaced unknown characters with ",[49,6734,6735],{},"?"," or ``, discarded bytes, applied a lossy normalization, or saved an already corrupted string through several destructive conversions, the missing information is no longer available to reconstruct.",[11,6738,6739],{},[1068,6740],{"alt":6741,"src":6742},"The same UTF-8 byte sequence becomes the word ‘Привет’ or several forms of mojibake depending on the selected decoder","\u002Fimages\u002Fblog\u002Finline_string-transformer.webp",[15,6744,6746],{"id":6745},"from-a-soviet-standard-to-koi8-r","From a Soviet standard to KOI8-R",[11,6748,6749,6750,6755,6756,6761],{},"One historical branch begins with ",[38,6751,6754],{"href":6752,"rel":6753},"https:\u002F\u002Fdocs.cntd.ru\u002Fdocument\u002F1200018572",[429],"GOST 19768-74",", adopted in the Soviet Union in 1974 for data interchange and processing. It described KOI-7 and KOI-8 character sets, and its early eight-bit table later became one of the sources for network Cyrillic encodings. Modern KOI8-R was not merely that GOST table under a new name: ",[38,6757,6760],{"href":6758,"rel":6759},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc1489.html",[429],"RFC 1489"," says that its repertoire also drew on several other Cyrillic sets.",[11,6763,6764],{},"RFC 1489 was published as an Informational document in July 1993 by Andrei Chernov of the Relcom Development Team. It did not declare KOI8-R an international standard. Instead, it documented an encoding already used by a large community and described it as a de facto standard for Unix and global network applications in the former Soviet Union, including Relcom.",[11,6766,6767,6768,6771,6772,6775],{},"KOI8-R is remembered for the way it places Russian letters. The lower half of the table matches ASCII, while many Cyrillic characters in the upper half sit opposite approximately corresponding Latin letters. If an old seven-bit mail route cleared the high bit, the phrase would not survive intact, but it might remain recognizable. ",[49,6769,6770],{},"Код для обмена информацией",", for example, becomes ",[49,6773,6774],{},"kOD DLQ OBMENA INFORMACIEJ"," after the high bit of every byte is cleared.",[11,6777,6778],{},"It is tempting to call that result transliteration, but it is only a consequence of the table. Letter case changes, the correspondences are approximate, and symbols without a useful ASCII counterpart can disappear or mutate. The property could help a reader identify a familiar phrase after a particular transmission failure, but it never guaranteed that arbitrary prose would remain legible. The same arrangement also meant that Cyrillic code values were not in ordinary alphabetic order, so correct sorting required encoding-aware software.",[15,6780,6782],{"id":6781},"cp866-for-dos-and-windows-1251-for-windows","CP866 for DOS and Windows-1251 for Windows",[11,6784,6785,6786,6791],{},"DOS followed a different branch. CP866 grew from the so-called Alternative Cyrillic encoding and retained much of the IBM PC box-drawing repertoire: lines, corners, and blocks that text-mode applications used to construct windows and tables. It shipped with the Russian version of MS-DOS 4.01 in 1990, and the current ",[38,6787,6790],{"href":6788,"rel":6789},"https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fwindows\u002Fwin32\u002Fintl\u002Fcode-page-identifiers",[429],"Microsoft identifier table"," classifies it as OEM Russian, Cyrillic (DOS). This is why CP866 still turns up in old text files, archives, games, terminal interfaces, and documents produced by DOS software.",[11,6793,6794],{},"Windows spread another table. Windows-1251 was already listed as the Windows Cyrillic code page in 1992 Windows 3.1 documentation, and it later became common in Windows applications and on Russian-language websites. Its main Russian letter blocks are close to alphabetic order, while additional Cyrillic letters and punctuation occupy other positions in the table.",[11,6796,6797],{},"The period is often summarized as three large environments: KOI8-R for Unix, email, and early networks; CP866 for DOS; and Windows-1251 for desktop Windows software and a substantial part of the web. That is a helpful map, not a complete census. ISO-8859-5, Mac Cyrillic, and local variants existed alongside them, so two programs could both support Cyrillic and still disagree about the bytes.",[15,6799,6801],{"id":6800},"how-the-encoding-war-produced-mojibake","How the encoding war produced mojibake",[11,6803,6804,6805,6808],{},"Multiple tables do not damage text by themselves. Trouble starts when encoding metadata disappears from the chain or one application ignores it. An email could arrive without a correct ",[49,6806,6807],{},"charset"," parameter, a web server could send one character set in its HTTP header while the HTML declared another, or an editor could open an old file using the operating system default. The same class of error appeared during CSV imports, database migrations, and connections configured with the wrong client encoding.",[11,6810,6811],{},"Sometimes the incorrectly decoded text was saved again. In one step, the original UTF-8 bytes became visible Windows-1251 characters; in the next, those characters were encoded as a new string. Multistage mojibake emerged from repeated conversions, and its appearance sometimes reveals the route the data took. This everyday incompatibility is what made the phrase “encoding war” feel appropriate. Each platform could behave consistently within its own boundaries while communication between platforms repeatedly exposed the mismatch.",[15,6813,6815],{"id":6814},"unicode-and-the-gradual-move-to-utf-8","Unicode and the gradual move to UTF-8",[11,6817,6818,6823,6824,6829],{},[38,6819,6822],{"href":6820,"rel":6821},"https:\u002F\u002Fwww.unicode.org\u002Fhistory\u002Fpublicationdates.html",[429],"Unicode 1.0"," was published in October 1991 with a different model: instead of giving every language a small, separate table, it assigned characters to a shared code space. UTF-8 became a way to represent those code points as bytes while preserving ASCII compatibility. Its specification evolved with Unicode and ISO\u002FIEC 10646, and the form used on the internet today is defined in documents including ",[38,6825,6828],{"href":6826,"rel":6827},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc3629.html",[429],"RFC 3629",", published in 2003.",[11,6831,6832,6833,6838],{},"A better standard did not produce an instant migration. Operating systems, editors, databases, server configurations, and existing archives all had to change. UTF-8 adoption grew throughout the 2000s and 2010s while old websites and documents continued to use Windows-1251, KOI8-R, or CP866. According to ",[38,6834,6837],{"href":6835,"rel":6836},"https:\u002F\u002Fw3techs.com\u002Ftechnologies\u002Fdetails\u002Fen-utf8",[429],"W3Techs",", UTF-8 was used by 99% of websites with a known character encoding in August 2026, but that statistic cannot rewrite the bytes in an old archive or backup.",[11,6840,6841],{},"Mojibake therefore still appears today. It usually comes from a legacy file, an export from an older database, an incorrectly imported CSV, a misleading HTTP header, or text that was previously opened and saved with the wrong encoding rather than from a modern document that failed without cause.",[15,6843,6845],{"id":6844},"what-string-transformer-actually-does","What String Transformer actually does",[11,6847,6848,6852],{},[38,6849,6851],{"href":6850},"\u002Ftools\u002Fstring-transformer","String Transformer"," can investigate such a string directly in the browser. Its automatic recovery tries common pairs of visible and presumed source encodings, including UTF-8, Windows-1251, KOI8-R, CP866, ISO-8859-5, and Mac Cyrillic. It scores the resulting strings and displays up to six plausible candidates for Cyrillic or Hebrew. The ranking is a heuristic rather than proof, so the final choice still has to be checked against the meaning of the text.",[11,6854,6855,6856,6859],{},"Manual mode lets you choose a character set for byte-oriented transformations such as URL encoding, Base32, Base45, Base64, hex, Ascii85, quoted-printable, and byte escapes. The available legacy sets depend on ",[49,6857,6858],{},"TextDecoder"," support in the current browser, and the interface disables options the browser cannot decode. Processing is local, so the entered text is not sent to the server.",[11,6861,6862],{},"The tool also changes letter case and naming styles, repairs text typed with the wrong English or Russian keyboard layout, transliterates Cyrillic, and transforms HTML entities, Unicode escapes, Morse code, and Braille. Keyboard-layout repair is separate from encoding recovery: in the first case the user pressed keys under the wrong layout, while in the second case a program interpreted the correct bytes through the wrong table.",[11,6864,6865,6866,6868,6869,6872],{},"For a quick test, paste ",[49,6867,6697],{}," and inspect the proposed recovery candidates, then type ",[49,6870,6871],{},"ghbdtn"," and apply the keyboard-layout correction separately. If a recovered candidate looks right, save it as a new UTF-8 file and keep the only copy of the original data untouched.",[15,6874,358],{"id":357},[357,6876],{":slug":6877},"[\"string-transformer\",\"json-toolbox\",\"hash-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":6879},[6880,6881,6882,6883,6884,6885,6886],{"id":6707,"depth":364,"text":6708},{"id":6745,"depth":364,"text":6746},{"id":6781,"depth":364,"text":6782},{"id":6800,"depth":364,"text":6801},{"id":6814,"depth":364,"text":6815},{"id":6844,"depth":364,"text":6845},{"id":357,"depth":364,"text":358},"2026-08-10","Why Cyrillic text breaks across KOI8-R, CP866, Windows-1251 and UTF-8, how mojibake happens, and when the original text can be recovered.",[6890,6893,6896,6899],{"q":6891,"a":6892},"What is mojibake?","Mojibake (文字化け) is garbled text produced when a sequence of bytes written in one encoding is decoded according to another. The bytes may still be intact, but the program assigns the wrong characters to them.",{"q":6894,"a":6895},"Which Cyrillic encodings were used before UTF-8?","KOI8-R was prominent on Unix and early networks, CP866 was common in DOS, and Windows-1251 spread with desktop Windows. This is only a useful shorthand: ISO-8859-5, Mac Cyrillic, and other character sets also existed, so the real compatibility landscape had more than three participants.",{"q":6897,"a":6898},"Why did KOI8-R partly survive the loss of the high bit?","KOI8-R places many Cyrillic letters so that clearing the high bit produces roughly corresponding ASCII letters. For example, «Код для обмена информацией» becomes 'kOD DLQ OBMENA INFORMACIEJ'. This is neither normal transliteration nor a guarantee that arbitrary text will remain readable, but a familiar phrase could sometimes still be recognized.",{"q":6900,"a":6901},"Can broken Cyrillic text be repaired automatically?","Sometimes. Recovery works when the original bytes are still represented and the mistaken conversion is reversible. If an application has replaced unknown characters with question marks or U+FFFD, truncated data, or performed several lossy saves, the missing information is no longer present in the garbled string.","\u002Fimages\u002Fblog\u002Fblog_string-transformer.webp",{},"\u002Fblog\u002Fen\u002Fstring-transformer",{"title":6689,"description":6888},"blog\u002Fen\u002Fstring-transformer",[6908,6909,6910,6911,6912,6913],"Mojibake explained","KOI8-R history","Windows-1251 vs UTF-8","Broken encoding recovery","Cyrillic encodings","Character encoding wars","VXhgLcATRVfQvBqlqh_ZFUVfr_RPK-IiZTTmCuYLZx8",{"id":6916,"title":6917,"body":6918,"date":7102,"description":7103,"extension":376,"faq":7104,"image":7117,"lang":394,"meta":7118,"navigation":396,"path":7119,"published":396,"readTime":1270,"seo":7120,"stem":7121,"tags":7122,"updated":405,"__hash__":7129},"blog\u002Fblog\u002Fen\u002Fmarkdown-history.md","How Markdown Became the Web's Plain-Text Language",{"type":8,"value":6919,"toc":7093},[6920,6923,6927,6940,6949,6956,6960,6973,6984,6993,6997,7008,7014,7018,7026,7035,7044,7048,7051,7054,7058,7072,7085,7088,7090],[11,6921,6922],{},"Markdown is easiest to notice when it behaves differently from what a writer expected. A list suddenly continues as a paragraph, indentation moves a block into the wrong place, or a line beginning with a hash is not treated as a heading. When the format works normally, its punctuation barely interrupts the reading experience, and that was its central design goal. In 2004, Markdown offered web writers readable source text and delegated the conversion to HTML to a small program. Two decades later, the same principle suits README files, documentation, notes, and interfaces that display language-model responses.",[15,6924,6926],{"id":6925},"it-began-as-both-a-format-and-a-perl-script","It began as both a format and a Perl script",[11,6928,6929,6930,6935,6936,6939],{},"On March 15, 2004, John Gruber published ",[38,6931,6934],{"href":6932,"rel":6933},"https:\u002F\u002Fdaringfireball.net\u002F2004\u002F03\u002Fintroducing_markdown",[429],"Introducing Markdown"," on Daring Fireball. He presented it as a text-to-HTML tool for web writers that could run as a plug-in for Movable Type and Blosxom or as a standalone Perl script. One name referred to two related things from the beginning: a way to write structured plain text and ",[49,6937,6938],{},"Markdown.pl",", the program that performed the conversion.",[11,6941,6942,6943,6948],{},"Gruber was not trying to replace HTML with another universal language. In the original ",[38,6944,6947],{"href":6945,"rel":6946},"https:\u002F\u002Fdaringfireball.net\u002Fprojects\u002Fmarkdown\u002Fsyntax",[429],"syntax documentation",", he separated their roles clearly: HTML is a publishing format, while Markdown is a writing format. When Markdown's deliberately small syntax could not express something, an author could insert regular HTML. That decision kept the format compact instead of copying all the complexity of web markup into it.",[11,6950,6951,6952,6955],{},"Gruber described plain text email as the largest source of inspiration. The ",[49,6953,6954],{},">"," character already suggested an email quotation, asterisks looked like emphasis, and hyphens naturally read as list markers. Setext, atx, Textile, reStructuredText, Grutatext, and EtText also influenced the syntax. Markdown was therefore not one isolated invention; it selected familiar text conventions that preserved their meaning without a visual editor.",[15,6957,6959],{"id":6958},"what-aaron-swartz-contributed","What Aaron Swartz contributed",[11,6961,6962,6963,6968,6969,6972],{},"Aaron Swartz contributed more than an ordinary beta user, but his role is best described in the language of the project itself. On the ",[38,6964,6967],{"href":6965,"rel":6966},"https:\u002F\u002Fdaringfireball.net\u002Fprojects\u002Fmarkdown\u002F",[429],"Markdown project page",", Gruber credits Swartz's ideas, feedback, and testing with substantially improving the syntax. The same page links Swartz's ",[49,6970,6971],{},"html2text",", which handled the reverse direction by turning HTML into readable Markdown-compatible text.",[11,6974,6975,6976,6979,6980,6983],{},"Swartz had previously published a structured text format called ",[49,6977,6978],{},"atx",". The original Markdown documentation lists atx among its influences and calls hash-prefixed headings ATX-style headings. That relationship is well documented, but it does not show that every heading rule was copied unchanged or that Swartz's contribution can be reduced to the ",[49,6981,6982],{},"#"," character. It is more accurate to describe him as a participant in the format's design and testing, while Gruber wrote the first converter and handled the public release.",[11,6985,6986,6987,6992],{},"Markdown continued to change after the March announcement. Version 1.0 left beta on August 28, 2004, and Gruber released ",[38,6988,6991],{"href":6989,"rel":6990},"https:\u002F\u002Fdaringfireball.net\u002F2004\u002F12\u002Fmarkdown_101",[429],"Markdown 1.0.1"," on December 17 with bug fixes and revised handling for several ambiguous cases. On the same day, the project moved from the GPL to a more permissive BSD-style license, making it easier for other software to incorporate the converter and its ideas.",[15,6994,6996],{"id":6995},"readable-syntax-is-not-necessarily-unambiguous-syntax","Readable syntax is not necessarily unambiguous syntax",[11,6998,6999,7000,7003,7004,7007],{},"Markdown had detailed documentation for headings, links, block quotes, lists, emphasis, and code, but it did not have a formal parser specification. That distinction was easy to miss in ordinary writing. ",[49,7001,7002],{},"**text**"," clearly resembled strong emphasis, ",[49,7005,7006],{},"# Heading"," looked like a heading, and a blank line separated paragraphs. Problems appeared when structures were nested or when more than one rule could explain the same line.",[11,7009,7010,7011,7013],{},"How many spaces should precede a nested list? Can a heading begin immediately after a paragraph? In an ambiguous line, does a list marker, code span, or link take precedence? The documentation did not always answer such questions, and the behavior of ",[49,7012,6938],{}," did not always match the prose. Authors of new parsers made different choices and introduced extensions for tables, strikethrough, footnotes, and fenced code blocks. Markdown consequently became a family of related dialects, and the same file could render differently in two systems.",[15,7015,7017],{"id":7016},"from-standard-markdown-to-commonmark","From Standard Markdown to CommonMark",[11,7019,7020,7021,507],{},"In 2014, a group of parser authors and engineers from large platforms began formalizing a compatible core language. The participants included Pandoc author John MacFarlane, Jeff Atwood, and people associated with GitHub, Reddit, Stack Overflow, and Meteor. The project was initially called Standard Markdown, but Gruber asked them not to use Markdown in a name that could imply a single official standard. In September 2014, it was ",[38,7022,7025],{"href":7023,"rel":7024},"https:\u002F\u002Ftalk.commonmark.org\u002Ft\u002Fstandard-markdown-is-now-commonmark\u002F434",[429],"renamed CommonMark",[11,7027,7028,7029,7034],{},"CommonMark did more than rewrite the old guide in stricter prose. Its ",[38,7030,7033],{"href":7031,"rel":7032},"https:\u002F\u002Fspec.commonmark.org\u002F0.31.2\u002F",[429],"specification"," contains hundreds of Markdown and HTML pairs that both explain a rule and test an implementation. It defines precedence between block and inline structures, whitespace handling, list boundaries, links, code spans, and other disputed areas. The current published specification is numbered 0.31.2, so CommonMark has not formally reached 1.0, but its rules already provide a practical foundation for many parsers.",[11,7036,7037,7038,7043],{},"GitHub does not use unmodified CommonMark. It specifies ",[38,7039,7042],{"href":7040,"rel":7041},"https:\u002F\u002Fgithub.github.com\u002Fgfm\u002F",[429],"GitHub Flavored Markdown"," as a strict superset of CommonMark, with extensions for tables, task lists, strikethrough, and additional autolinks. This illustrates how the modern ecosystem works: CommonMark supplies a compatible core, and an individual platform adds features needed by its interface.",[15,7045,7047],{"id":7046},"why-the-format-outlived-its-original-environment","Why the format outlived its original environment",[11,7049,7050],{},"Markdown has lasted not because one parser defeated every alternative, but because its central advantage survives across dialects. Structure remains visible in the source, a file opens in an ordinary text editor, and conversion does not require a closed binary format. Those properties work well with version control, technical documentation, static site generators, and applications that keep notes as text files.",[11,7052,7053],{},"The same reasoning helps explain Markdown-like output in AI interfaces. Many chat clients can render headings, lists, links, and code blocks while leaving the underlying character sequence understandable without rendering. It would still be inaccurate to claim that every language model uses Markdown internally or must always follow it. A model generates text, and the application decides which constructs to recognize, permit, and display safely.",[15,7055,7057],{"id":7056},"what-our-editor-does","What our editor does",[11,7059,510,7060,7064,7065,698,7068,7071],{},[38,7061,7063],{"href":7062},"\u002Ftools\u002Fmarkdown-editor","Markdown Editor"," runs in the browser and places a live preview beside the source editor. Its toolbar inserts headings, emphasis, links, images, lists, block quotes, task lists, code blocks, and tables. The tool also provides undo and redo, word and character counts, synchronized scrolling, an HTML view with line numbers, HTML copying, and ",[49,7066,7067],{},".md",[49,7069,7070],{},".html"," downloads.",[11,7073,7074,7075,7080,7081,7084],{},"Rendering uses ",[38,7076,7079],{"href":7077,"rel":7078},"https:\u002F\u002Fmarked.js.org\u002F",[429],"Marked"," with the GFM and ",[49,7082,7083],{},"breaks"," options enabled. Tables, task lists, strikethrough, and fenced code blocks are supported, while a single newline is rendered as a line break. That behavior is convenient in an interactive editor, but it is not guaranteed to match every other Markdown parser.",[11,7086,7087],{},"There are two important limitations. The editor does not persist the document across page reloads, so download or copy the result before closing the tab. Marked also does not sanitize generated HTML, and our preview does not add a separate sanitizer. Use the editor for text you wrote or trust, and do not treat unknown Markdown as safe HTML for another site. Conversion itself happens locally, although remote images or other resources included in the preview can still be requested by the browser from their specified addresses.",[15,7089,358],{"id":357},[357,7091],{":slug":7092},"[\"markdown-editor\",\"license-builder\",\"json-toolbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":7094},[7095,7096,7097,7098,7099,7100,7101],{"id":6925,"depth":364,"text":6926},{"id":6958,"depth":364,"text":6959},{"id":6995,"depth":364,"text":6996},{"id":7016,"depth":364,"text":7017},{"id":7046,"depth":364,"text":7047},{"id":7056,"depth":364,"text":7057},{"id":357,"depth":364,"text":358},"2026-08-08","Markdown's history from John Gruber's first Perl script and Aaron Swartz's contribution to CommonMark, GitHub Flavored Markdown, and modern editors.",[7105,7108,7111,7114],{"q":7106,"a":7107},"Who created Markdown?","John Gruber wrote the first Markdown.pl converter, defined the format's goals, and released it through Daring Fireball in 2004. Aaron Swartz discussed the syntax, contributed ideas, tested Markdown, and wrote the reverse html2text converter. The most accurate short account is that Gruber created and implemented Markdown with substantial input from Swartz.",{"q":7109,"a":7110},"Why did Markdown become popular?","Markdown keeps its source understandable without rendering while still converting into structured HTML. It was easy to integrate into blogs, repositories, forums, and documentation generators, and the converter's permissive license helped other implementations appear. Its popularity produced several compatible dialects rather than one universal Markdown parser.",{"q":7112,"a":7113},"How does CommonMark differ from original Markdown?","The original Markdown documentation described its main constructs but left many combinations of lists, indentation, links, and blocks open to interpretation. CommonMark turns those edge cases into detailed rules and hundreds of examples that double as conformance tests. GitHub Flavored Markdown builds on CommonMark and adds extensions such as tables and task lists.",{"q":7115,"a":7116},"Why do AI responses often look like Markdown?","Many chat interfaces recognize Markdown-like headings, lists, links, and code blocks because those structures remain readable in a raw text response. The model produces a sequence of text, while the client application decides how to render it. Markdown is therefore convenient for AI interfaces, but it is not a mandatory format for every model or every answer.","\u002Fimages\u002Fblog\u002Fblog_markdown-history.webp",{},"\u002Fblog\u002Fen\u002Fmarkdown-history",{"title":6917,"description":7103},"blog\u002Fen\u002Fmarkdown-history",[7123,7124,7125,7126,7127,7128],"Markdown history","Who invented Markdown","Markdown vs CommonMark","Why Markdown matters","Aaron Swartz Markdown","John Gruber","QHVlEUmgST6GiZSaH3fYkW2UOSJNFaHOIzRS-PF3hMA",{"id":7131,"title":7132,"body":7133,"date":7324,"description":7325,"extension":376,"faq":7326,"image":7342,"lang":394,"meta":7343,"navigation":396,"path":7344,"published":396,"readTime":1270,"seo":7345,"stem":7346,"tags":7347,"updated":405,"__hash__":7354},"blog\u002Fblog\u002Fen\u002Fvisual-cryptography.md","The Secret Hidden Across Two Images",{"type":8,"value":7134,"toc":7314},[7135,7138,7141,7145,7164,7167,7171,7174,7177,7180,7186,7190,7193,7196,7200,7203,7206,7210,7220,7223,7226,7272,7275,7279,7282,7294,7298,7303,7306,7309,7311],[11,7136,7137],{},"Two transparent sheets can each look like ordinary black-and-white noise, yet reveal a word or picture when aligned precisely. That is the central idea of visual cryptography: divide a secret into images that are useless separately, while allowing the human eye to recover the combined result without a computer or a cryptographic calculation.",[11,7139,7140],{},"The idea matters not because transparencies are more convenient than digital encryption. Naor and Shamir showed something more fundamental: threshold secret sharing could be designed directly for human vision, with explicit rules for how many shares must meet and what an incomplete set is allowed to reveal.",[15,7142,7144],{"id":7143},"from-eurocrypt-1994-to-the-published-construction","From EUROCRYPT 1994 to the published construction",[11,7146,7147,7148,7151,7152,7157,7158,7163],{},"Moni Naor and Adi Shamir presented a preliminary version of ",[26,7149,7150],{},"Visual Cryptography"," at EUROCRYPT 1994 in Perugia. The paper appeared in 1995 in ",[38,7153,7156],{"href":7154,"rel":7155},"https:\u002F\u002Fdoi.org\u002F10.1007\u002FBFb0053419",[429],"volume 950 of Lecture Notes in Computer Science",", pages 1–12. The authors defined a general K-of-N construction in which any K shares reveal the picture, while K−1 shares disclose nothing about it. Their ",[38,7159,7162],{"href":7160,"rel":7161},"https:\u002F\u002Fwww.wisdom.weizmann.ac.il\u002F~\u002Fnaor\u002FPAPERS\u002Fvis.pdf",[429],"original paper"," covers not only the simplest 2-of-2 case, but also higher thresholds, continuous-tone images, and shares that resemble meaningful pictures.",[11,7165,7166],{},"Visual cryptography is often mentioned alongside Shamir's 1979 secret sharing scheme, but the two are not the same construction in different clothing. Shamir's original shares are numbers in a finite field, and polynomial interpolation recovers the secret. Visual cryptography uses transparent subpixel patterns as shares, while optical stacking performs the recovery operation.",[15,7168,7170],{"id":7169},"turning-one-pixel-into-two-shares","Turning one pixel into two shares",[11,7172,7173],{},"In the practical 2-of-2 version, every pixel of a black-and-white secret expands into a 2×2 block. Each block contains two black and two white subpixels, with six possible arrangements. For every source pixel, the encoder chooses one arrangement at random and places it in the first share.",[11,7175,7176],{},"For a white secret pixel, the second share receives the same pattern. Stacking the sheets leaves half of the block white, so the eye sees gray. For a black secret pixel, the second share receives the complementary pattern, making all four positions black when stacked. The contrast between blocks containing two and four black subpixels forms the visible image.",[11,7178,7179],{},"The original paper also describes a construction with two subpixels per source pixel, but a 2×2 block preserves the image's aspect ratio. The tradeoff is easy to see: every pixel now occupies four times the area, white reconstructs as gray, and even a small alignment error can reduce legibility.",[11,7181,7182],{},[1068,7183],{"alt":7184,"src":7185},"For a white secret pixel, two identical 2×2 patterns retain two white subpixels when optically stacked; for a black pixel, complementary patterns produce a fully black block","\u002Fimages\u002Fblog\u002Finline_visual-cryptography.webp",[15,7187,7189],{"id":7188},"the-boundary-of-perfect-secrecy","The boundary of perfect secrecy",[11,7191,7192],{},"A single share looks the same whether the secret pixel is white or black. In either case it contains a random pattern with two black and two white subpixels. Someone holding only one share sees the same distribution over the six possible patterns and cannot infer the original pixel's color, even with unlimited computing power. This is the precise sense in which the classical scheme provides perfect secrecy.",[11,7194,7195],{},"That guarantee applies to pixel content under correct random generation. It does not hide the dimensions or shape of the medium, and reusing a random share for different secrets violates the original security model. A physical implementation adds practical constraints as well: print quality, transparency, and alignment affect the visible result even though they do not change the mathematical shares.",[15,7197,7199],{"id":7198},"what-followed-the-original-paper","What followed the original paper",[11,7201,7202],{},"Naor and Shamir's paper established a field of its own. Later constructions explored different access thresholds, lower pixel expansion, color and continuous tones, improved contrast, and shares disguised as ordinary images. These objectives interact: improving one parameter often costs share size, reconstructed image quality, or stricter alignment requirements.",[11,7204,7205],{},"Classical transparencies did not become an everyday replacement for encrypted messages, but the model remains useful. It makes the distinction between computational security, which relies on an attack being impractical, and information-theoretic security, where an incomplete set of shares simply contains no information about the secret.",[15,7207,7209],{"id":7208},"what-the-browser-tool-actually-does","What the browser tool actually does",[11,7211,510,7212,7215,7216,7219],{},[38,7213,7150],{"href":7214},"\u002Ftools\u002Fvisual-cryptography"," tool borrows the broader idea of separate visual carriers, but it is not an implementation of Naor and Shamir's optical construction. It first turns text into a digital grid. In ",[26,7217,7218],{},"Two shares"," mode it then splits that grid with a bitwise XOR operation. Both images must be given to the decoder to recover the message; physically stacking them will not make the secret appear.",[11,7221,7222],{},"All processing runs locally in the browser. Text is encoded as UTF-8, and a Deflate result is retained only when it makes the payload smaller. If password protection is enabled, the tool encrypts the data with AES-256-GCM and derives the key through PBKDF2-SHA-256 with 200,000 iterations. This does not turn a weak password into a strong one, so sensitive data still requires a good passphrase. Without password protection, a single image is an encoding, not a confidentiality mechanism.",[11,7224,7225],{},"The payload also receives a CRC32 checksum and Reed–Solomon blocks. They serve different purposes: the checksum detects damage, while parity symbols can repair some errors. The header always has eight Reed–Solomon parity bytes, while payload protection depends on the selected profile.",[75,7227,7228,7238],{},[78,7229,7230],{},[81,7231,7232,7235],{},[84,7233,7234],{},"Profile",[84,7236,7237],{},"Added protection",[91,7239,7240,7248,7256,7264],{},[81,7241,7242,7245],{},[96,7243,7244],{},"Compact",[96,7246,7247],{},"CRC32 with no Reed–Solomon payload parity",[81,7249,7250,7253],{},[96,7251,7252],{},"Balanced",[96,7254,7255],{},"Up to 32 parity bytes per block",[81,7257,7258,7261],{},[96,7259,7260],{},"Robust",[96,7262,7263],{},"Up to 64 parity bytes per block",[81,7265,7266,7269],{},[96,7267,7268],{},"Maximum",[96,7270,7271],{},"Three interleaved copies of each cell and up to 96 parity bytes per block",[11,7273,7274],{},"Maximum is designed around recovering from roughly 10% random cell damage, but that is not a universal promise for every image. The outcome depends on payload size, profile, damage pattern, and source quality. The built-in simulator therefore does more than display an abstract score: it introduces 1% to 35% random errors or a contiguous scratch, then runs the actual decoder against the damaged grid.",[15,7276,7278],{"id":7277},"from-four-levels-to-glyph-mosaic","From four levels to Glyph Mosaic",[11,7280,7281],{},"In the standard modes, one cell stores two bits using one of four levels. The grid can be fitted to square, landscape, or wide framing, with unused cells filled by deterministic noise. Fourteen render styles change how those same values look, using flat pixels, halftones, contours, mazes, Truchet arcs, isometric blocks, or topographic lines. Palettes and decorative elements change the presentation, but do not by themselves increase the underlying payload.",[11,7283,7284,7287,7288,7293],{},[26,7285,7286],{},"Glyph Mosaic",", inspired by ",[38,7289,7292],{"href":7290,"rel":7291},"https:\u002F\u002Fgithub.com\u002Fsz3\u002Flibcimbar",[429],"libcimbar",", uses a different alphabet. A value is represented by both an 8×8 symbol template and its ink color. Its modes combine 16 or 32 shapes with 4 or 8 colors to store 6 to 8 bits per cell. The templates maintain substantial Hamming distance, and the decoder probes multiple possible grid sizes, which helps with moderate scaling and blur. The format does not have QR-style finder patterns or perspective correction, however, so an arbitrarily rotated photo, severe crop, or very small cells may fail to decode.",[15,7295,7297],{"id":7296},"why-two-digital-shares-still-need-qualifications","Why two digital shares still need qualifications",[11,7299,2787,7300,7302],{},[26,7301,7218],{}," mode, the tool uses the browser's cryptographic random generator to create a uniformly random grid A, then computes each B cell as the XOR of A and the encoded data cell. With fresh randomness and a fixed grid size, the distribution of either share does not depend on the source cell, so A or B alone cannot recover the grid content.",[11,7304,7305],{},"The files still reveal their dimensions, chosen style, palette, and some settings, and the two shares need matching geometry before they can be combined. This is why the broad statement that an image “reveals nothing” needs a boundary: the cell values are hidden, but external properties of the files remain observable. After recombination, the decoder reads the header, repairs errors within the selected profile's capacity, checks integrity, decrypts when necessary, and returns the original text.",[11,7307,7308],{},"The result is exported as an indexed PNG with a four- or eight-color palette, adaptive per-row filters, and Deflate compression. It can also be copied as a Base64 data URL. The classical idea of two individually useless carriers survives in digital form, but its recovery mechanism and security boundaries are different.",[15,7310,358],{"id":357},[357,7312],{":slug":7313},"[\"visual-cryptography\",\"shamir-secret\",\"qr-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":7315},[7316,7317,7318,7319,7320,7321,7322,7323],{"id":7143,"depth":364,"text":7144},{"id":7169,"depth":364,"text":7170},{"id":7188,"depth":364,"text":7189},{"id":7198,"depth":364,"text":7199},{"id":7208,"depth":364,"text":7209},{"id":7277,"depth":364,"text":7278},{"id":7296,"depth":364,"text":7297},{"id":357,"depth":364,"text":358},"2026-08-06","How Naor and Shamir's visual cryptography works, and how a modern browser codec differs from the classical construction.",[7327,7330,7333,7336,7339],{"q":7328,"a":7329},"What is visual cryptography?","It is a way to split a secret image into noise-like shares that reveal the picture when stacked. In the classical 2-of-2 construction, either share is statistically independent of the hidden pixel's color, while the combined result can be read by eye without computation.",{"q":7331,"a":7332},"Who invented visual cryptography?","Moni Naor and Adi Shamir presented a preliminary version of Visual Cryptography at EUROCRYPT 1994. The paper appeared in 1995 in volume 950 of Lecture Notes in Computer Science.",{"q":7334,"a":7335},"Why does one classical share reveal nothing?","For every pixel, the encoder randomly selects one of six 2×2 patterns containing two black and two white subpixels. A single share has the same distribution for a white or black secret pixel, so its color cannot be inferred. This assumes correct randomness and does not hide the image dimensions or other external properties.",{"q":7337,"a":7338},"How does the browser tool differ from the classical scheme?","The classical construction uses monochrome transparencies and reveals an image through physical stacking. The browser tool encodes text into a digital grid and can split that grid into two XOR shares that software must recombine. It also offers password encryption, error correction, and artistic render styles.",{"q":7340,"a":7341},"How secure is Two shares mode?","For a fixed-size grid, the tool generates a uniformly random Share A and computes Share B as A XOR the data. With fresh randomness, either share alone hides the cell values, but dimensions, the selected style, and other image settings remain visible. A single image needs password encryption if confidentiality is required.","\u002Fimages\u002Fblog\u002Fblog_visual-cryptography.webp",{},"\u002Fblog\u002Fen\u002Fvisual-cryptography",{"title":7132,"description":7325},"blog\u002Fen\u002Fvisual-cryptography",[7348,7349,7350,7351,7352,7353],"Visual cryptography","Naor-Shamir","Secret sharing without computers","Image splitting","EUROCRYPT","Cryptography without computation","pdv0h6X_8p-4BUWzZPHRufnHIuU2idJdYASy8WJr6Ec",{"id":7356,"title":7357,"body":7358,"date":7552,"description":7553,"extension":376,"faq":7554,"image":7567,"lang":394,"meta":7568,"navigation":396,"path":7569,"published":396,"readTime":1270,"seo":7570,"stem":7571,"tags":7572,"updated":405,"__hash__":7579},"blog\u002Fblog\u002Fen\u002Fshamir-secret-sharing.md","How Mathematics Splits a Secret Into Useless Pieces",{"type":8,"value":7359,"toc":7544},[7360,7363,7367,7376,7379,7385,7389,7392,7399,7406,7412,7416,7427,7433,7450,7453,7459,7466,7482,7486,7495,7504,7513,7517,7524,7530,7533,7539,7541],[11,7361,7362],{},"Suppose an important key is kept in a single safe. The only copy can be lost, but making ordinary duplicates creates another compromise point every time. What is needed is a different arrangement: distribute data among several custodians so that a predetermined number can recover the secret, while a smaller group learns nothing useful about it. Adi Shamir described just such a threshold construction in 1979.",[15,7364,7366],{"id":7365},"two-pages-that-changed-key-management","Two pages that changed key management",[11,7368,7369,7370,7375],{},"The paper ",[38,7371,7374],{"href":7372,"rel":7373},"https:\u002F\u002Fdl.acm.org\u002Fdoi\u002F10.1145\u002F359168.359176",[429],"“How to Share a Secret”"," occupied pages 612 and 613 of the November issue of Communications of the ACM. Shamir proposed representing the secret as a number and placing it in the constant term of a random polynomial of degree at most K−1. The polynomial is then evaluated at N distinct nonzero points, and each pair consisting of a point number and its value becomes a share.",[11,7377,7378],{},"Any K points uniquely determine a polynomial of degree at most K−1, so they are enough to evaluate it at zero and recover the secret. With only K−1 points, one equation is missing. For every possible secret in the field there is a compatible set of coefficients, and uniform random coefficients make all those possibilities equally likely. This is the precise probabilistic sense in which the classical scheme offers perfect secrecy below the threshold; it does not merely make a brute-force attack expensive.",[11,7380,7381,7382,7384],{},"Shamir's original construction performs the calculations modulo a prime ",[49,7383,11],{}," chosen larger than both the secret and the number of shares. This creates a finite field in which addition, multiplication, and division are exact, with no rounding errors, while each share remains comparable in size to the secret.",[15,7386,7388],{"id":7387},"how-it-works-in-plain-terms","How it works in plain terms",[11,7390,7391],{},"Consider a teaching example with the secret 42 and a (3, 5) scheme: five shares are created and any three must recover the value. In the familiar geometric analogy, 42 is where a parabola crosses the Y axis, two other coefficients are random, and five points on the parabola are handed to the participants.",[11,7393,7394,7395,7398],{},"Three points determine one parabola and therefore reveal its value at ",[49,7396,7397],{},"x = 0",". On an ordinary real-valued graph, infinitely many parabolas with different Y intercepts pass through only two points. That picture makes the threshold intuitive, but the actual algorithm does not work with a smooth curve or real coordinates. It uses a finite set of field elements, where the exact guarantee is that every possible secret remains equally likely.",[11,7400,7401,7402,7405],{},"Text is first encoded as a sequence of bytes. A separate polynomial with independent random coefficients is built for every byte, and all those polynomials are evaluated at the same share number ",[49,7403,7404],{},"x",". A participant therefore receives one point number followed by a sequence containing one field value for every byte of the original text.",[11,7407,7408],{},[1068,7409],{"alt":7410,"src":7411},"A secret byte becomes five GF(256) shares; shares 1, 3, and 5 recover the value at zero, while two shares leave 256 possible values","\u002Fimages\u002Fblog\u002Finline_shamir-secret-sharing.webp",[15,7413,7415],{"id":7414},"the-formula-how-the-secret-comes-back","The formula: how the secret comes back",[11,7417,7418,7419,7422,7423,7426],{},"The browser tool uses ",[26,7420,7421],{},"GF(256)",", so one field element maps exactly to one byte. For a byte ",[49,7424,7425],{},"b",", it constructs:",[1564,7428,7431],{"className":7429,"code":7430,"language":4815,"meta":363},[4813],"f(x) = b ⊕ a₁·x ⊕ a₂·x² ⊕ … ⊕ a_(K−1)·x^(K−1)\n",[49,7432,7430],{"__ignoreMap":363},[11,7434,4820,7435,7438,7439,7442,7443,7446,7447,7449],{},[49,7436,7437],{},"b = f(0)",", while the remaining coefficients come from the browser's cryptographic random generator. The ",[49,7440,7441],{},"⊕"," symbol denotes addition in GF(256), which is bitwise XOR; multiplication and exponentiation also follow field rules rather than ordinary integer arithmetic. Shares are evaluated at ",[49,7444,7445],{},"x = 1, 2, …, N",", because a share at ",[49,7448,7397],{}," would expose the constant term directly.",[11,7451,7452],{},"Recovery does not need to reconstruct every coefficient explicitly. Lagrange interpolation can evaluate the polynomial directly at zero:",[1564,7454,7457],{"className":7455,"code":7456,"language":4815,"meta":363},[4813],"b = ⊕ᵢ f(xᵢ)·Lᵢ(0),\nwhere Lᵢ(0) = ∏_(j≠i) xⱼ \u002F (xᵢ ⊕ xⱼ)\n",[49,7458,7456],{"__ignoreMap":363},[11,7460,7461,7462,7465],{},"In a field of characteristic 2, subtraction is the same as addition, so the difference ",[49,7463,7464],{},"xᵢ − xⱼ"," is written as XOR. Distinct nonzero share numbers keep the denominator nonzero, and every nonzero field element has one multiplicative inverse.",[11,7467,7468,7469,7473,7474,7477,7478,7481],{},"The implementation represents GF(256) in the same way that ",[38,7470,4538],{"href":7471,"rel":7472},"https:\u002F\u002Fnvlpubs.nist.gov\u002Fnistpubs\u002FFIPS\u002FNIST.FIPS.197-upd1.pdf",[429]," specifies for AES: binary polynomial products are reduced modulo the irreducible polynomial ",[49,7475,7476],{},"x⁸ + x⁴ + x³ + x + 1",", written as ",[49,7479,7480],{},"0x11b",". Its multiplication and division tables use 3 as a primitive element. This does not mean that the tool encrypts data with AES; the two constructions merely use the same finite field.",[15,7483,7485],{"id":7484},"where-it-is-used-today","Where it is used today",[11,7487,7488,7489,7494],{},"A well-documented production example is ",[38,7490,7493],{"href":7491,"rel":7492},"https:\u002F\u002Fdeveloper.hashicorp.com\u002Fvault\u002Fdocs\u002Fconcepts\u002Fseal",[429],"HashiCorp Vault",". With a Shamir seal, Vault divides an unseal key into shares and requires the configured threshold to unlock the system. It does not split the whole secrets database; it distributes a relatively short key that protects the next layer of encryption keys.",[11,7496,7497,7498,7503],{},"The wallet ecosystem has ",[38,7499,7502],{"href":7500,"rel":7501},"https:\u002F\u002Fgithub.com\u002Fsatoshilabs\u002Fslips\u002Fblob\u002Fmaster\u002Fslip-0039.md",[429],"SLIP-0039",", a separate standard for mnemonic shares used to back up a master secret. It is not an ordinary BIP-39 phrase cut into groups of words, nor is it automatically compatible with every program that implements polynomial secret sharing. A real backup procedure must specify the standard, the share format, and a tested recovery path in another compatible application.",[11,7505,7506,7507,7512],{},"Threshold signatures, multisig, and key ceremonies pursue a similar organizational goal, but they should not automatically be labeled Shamir secret sharing. ",[38,7508,7511],{"href":7509,"rel":7510},"https:\u002F\u002Fwww.iana.org\u002Fdnssec\u002Fprocedures\u002Fksk-operator\u002FKSK_Key_Management_Policy_v3.8.pdf",[429],"IANA's root DNSSEC policy"," requires three of seven Crypto Officer credentials to activate an HSM, and all cryptographic operations involving the private KSK must run inside a validated device. The same policy separately specifies five-of-seven threshold secret sharing for the Storage Master Key or Domain Key without naming Shamir's algorithm. A threshold ceremony is therefore not evidence that the signing key itself has been split with Shamir polynomials.",[15,7514,7516],{"id":7515},"what-our-tool-actually-does","What our tool actually does",[11,7518,7519,7523],{},[38,7520,7522],{"href":7521},"\u002Ftools\u002Fshamir-secret","Shamir's Secret Sharing"," encodes the entered text as UTF-8 and splits each byte over GF(256). It can create between 2 and 255 shares with a threshold from 2 through N, copy their Base64 representation, save individual QR codes as PNG or SVG, or download a ZIP containing QR images, a text list, and metadata. During recovery it accepts share strings and QR images, removes duplicate point numbers, and checks that the stated threshold has been met.",[11,7525,7526,7527,7529],{},"Every share contains a format version, threshold K, random eight-byte set identifier, point number ",[49,7528,7404],{},", CRC32 of the original text, and the polynomial values. The identifier helps detect an accidentally mixed set, while CRC32 catches many forms of damage after reconstruction. Neither value is a digital signature or authentication code, however; someone who can modify a share can also recompute an unkeyed checksum.",[11,7531,7532],{},"The format has two further limitations worth making explicit. Share length reveals secret length, and the CRC32 stored in every share lets an observer test guesses about short or predictable input. The code also forces the leading coefficient to be nonzero so that every byte polynomial has exactly degree K−1. With K−1 shares, that restriction rules out one byte value, so the classical zero-leakage proof no longer applies literally. The polynomial payload reconstructs correctly, but this implementation should not be presented as a strict, independently verified perfect-secret-sharing system.",[11,7534,7535,7536,7538],{},"All calculations happen in the open tab using ",[49,7537,5504],{},", and the tool itself does not send the secret to a server. A loaded web page still executes inside an ordinary browser, though, so long-lived master keys and real wallet secrets deserve an audited implementation of a recognized format, an isolated device, and a recovery drill performed in advance. Every share also needs protection, because collecting the threshold reveals the entire original secret.",[15,7540,358],{"id":357},[357,7542],{":slug":7543},"[\"shamir-secret\",\"visual-cryptography\",\"key-pair-generator\",\"encryption-toolbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":7545},[7546,7547,7548,7549,7550,7551],{"id":7365,"depth":364,"text":7366},{"id":7387,"depth":364,"text":7388},{"id":7414,"depth":364,"text":7415},{"id":7484,"depth":364,"text":7485},{"id":7515,"depth":364,"text":7516},{"id":357,"depth":364,"text":358},"2026-08-04","How Adi Shamir's threshold scheme uses finite-field interpolation, where it is applied, and how the browser tool differs from the classical construction.",[7555,7558,7561,7564],{"q":7556,"a":7557},"What is Shamir's Secret Sharing?","It is a threshold cryptographic scheme that divides a secret into N shares. Any K valid shares recover the secret, while in the classical construction with uniformly chosen coefficients, the distribution of any K−1 shares is statistically independent of it. Adi Shamir described the scheme in his 1979 paper 'How to Share a Secret'.",{"q":7559,"a":7560},"How does a (K, N) threshold scheme work?","The secret becomes the constant term of a random polynomial of degree at most K−1, and the N shares are its values at distinct nonzero points. Lagrange interpolation over any K points recovers the polynomial's value at zero, which is the secret.",{"q":7562,"a":7563},"Where is secret sharing used?","It is used for distributed key custody and recovery. HashiCorp Vault, for example, can split an unseal key into shares, while SLIP-0039 defines an interoperable mnemonic-share format for wallet master-secret backups. Multisig, threshold signatures, and threshold access to an HSM solve related organizational problems but do not necessarily use Shamir's scheme.",{"q":7565,"a":7566},"Should I use this browser tool for critical keys?","The tool runs locally and is useful for studying the mechanism, but its format is not SLIP-0039 and has not been independently audited. Each share also contains the secret's CRC32, and the leading coefficient is restricted to nonzero values, so the strict zero-leakage guarantee for K−1 shares does not apply to this implementation. Use an audited specialist product on an isolated device for critical keys.","\u002Fimages\u002Fblog\u002Fblog_shamir-secret.webp",{},"\u002Fblog\u002Fen\u002Fshamir-secret-sharing",{"title":7357,"description":7553},"blog\u002Fen\u002Fshamir-secret-sharing",[7573,7574,7575,7576,7577,7578],"Secret sharing","Shamir's scheme","Lagrange interpolation","Threshold cryptography","K-of-N scheme","Seed phrase backup","6VjuRwnD6xoj2CMfXujbh0V7HKWLw7F2cIKnLkK50S4",{"id":7581,"title":7582,"body":7583,"date":7821,"description":7822,"extension":376,"faq":7823,"image":7839,"lang":394,"meta":7840,"navigation":396,"path":7841,"published":396,"readTime":4389,"seo":7842,"stem":7843,"tags":7844,"updated":405,"__hash__":7851},"blog\u002Fblog\u002Fen\u002Fjwt-history.md","JWT Does Not Encrypt Data: How to Read and Validate a Token",{"type":8,"value":7584,"toc":7811},[7585,7592,7595,7599,7608,7611,7614,7618,7624,7627,7633,7636,7642,7645,7658,7662,7665,7668,7684,7688,7697,7700,7708,7712,7719,7722,7736,7748,7752,7755,7764,7768,7786,7792,7803,7806,7808],[11,7586,7587,7588,7591],{},"A string such as ",[49,7589,7590],{},"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.p0W4T..."," looks as though encryption is hiding its contents. That is not true for the most common kind of JWT: anyone can turn the first two Base64URL segments back into JSON without knowing a secret or private key. The protection in such a token is not secrecy but the ability to detect a change to its claims.",[11,7593,7594],{},"This does not mean that every JWT consists of three readable parts. JWT defines a way to represent claims, while the related JWS and JWE standards give those claims their cryptographic form. Their distinction is practical rather than merely terminological because it determines which properties a token provides and what its recipient still needs to check.",[15,7596,7598],{"id":7597},"from-a-2010-draft-to-the-jose-family","From a 2010 draft to the JOSE family",[11,7600,7601,7602,7607],{},"The first JSON Web Token draft appeared in the IETF Datatracker in December 2010. Work later moved into the OAuth Working Group and became ",[38,7603,7606],{"href":7604,"rel":7605},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc7519.html",[429],"RFC 7519"," in May 2015. The related JOSE specifications arrived at the same time: JWS for signatures and message authentication codes, JWE for encryption, JWK for describing keys, and JWA for naming algorithms.",[11,7609,7610],{},"JWT had a deliberately limited job. It provided a compact, URL-safe representation of claims exchanged between two parties. The specification did not define a login system, server session, or user's permissions, and it did not make an arbitrary claims set trustworthy on its own. Those meanings come from the protocol that uses the token and from the receiving application's rules.",[11,7612,7613],{},"The RFC's publication date is therefore not the beginning of the whole idea. JWT drafts were already being used by early OpenID Connect work while the JOSE set moved through standardization. The 2015 documents established a shared format and vocabulary rather than inventing every token use case at once.",[15,7615,7617],{"id":7616},"jwt-jws-and-jwe-solve-different-problems","JWT, JWS, and JWE solve different problems",[11,7619,7620,7623],{},[38,7621,7606],{"href":7604,"rel":7622},[429]," defines a JWT Claims Set as a JSON object. That object can be the payload of a JWS or the plaintext of a JWE. A JWS protects integrity and origin with a digital signature or MAC, whereas a JWE provides confidentiality and integrity for encrypted content. A signed JWT can also be encrypted when both properties are required, producing a nested JWT.",[11,7625,7626],{},"JWS Compact Serialization is the form most commonly seen in APIs and debugging tools. It has three Base64URL segments containing a protected header, payload, and signature or MAC value. When the payload is a JWT Claims Set, the first two segments can indeed be read without a key. Base64URL merely maps bytes onto an alphabet suitable for URLs; it makes no attempt to conceal information.",[11,7628,7629],{},[1068,7630],{"alt":7631,"src":7632},"Compact JWS carrying a JWT Claims Set and verified with HMAC-SHA256","\u002Fimages\u002Fblog\u002Finline_jwt-history.webp",[11,7634,7635],{},"The illustration shows the specific HS256 case. The protected header and claims are encoded separately, then HMAC-SHA256 processes the first two segments and their separating period. Its result becomes the third segment:",[1564,7637,7640],{"className":7638,"code":7639,"language":4815,"meta":363},[4813],"JWS = base64url(header) + \".\" + base64url(payload) + \".\" + base64url(MAC)\n\nMAC = HMAC-SHA256(base64url(header) + \".\" + base64url(payload), secret)\n",[49,7641,7639],{"__ignoreMap":363},[11,7643,7644],{},"Compact JWE has a different structure with five segments: header, encrypted key, initialization vector, ciphertext, and authentication tag. Its claims cannot be recovered without a key. The familiar warning that “a JWT payload is always public” is consequently too broad; it applies to an ordinary unencrypted JWS, not to JWE.",[11,7646,7647,7648,7651,7652,7657],{},"The standard also describes an unsecured JWT with ",[49,7649,7650],{},"alg: none"," and an empty third segment. This is not a fallback to use after verification fails but a mode for an explicitly agreed context without cryptographic protection. ",[38,7653,7656],{"href":7654,"rel":7655},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc8725.html",[429],"RFC 8725"," recommends that libraries reject such tokens unless the application has deliberately requested them.",[15,7659,7661],{"id":7660},"a-signature-authenticates-bytes-not-the-truth-of-every-claim","A signature authenticates bytes, not the truth of every claim",[11,7663,7664],{},"With HS256, the issuer and every verifier know the same HMAC secret. This can be convenient within one trust boundary, but any recipient of that secret is technically able to mint tokens of its own. RS256, PS256, and ES256 separate those roles: the issuer signs with a private key, while services verify with the public key and do not gain signing authority.",[11,7666,7667],{},"None of these algorithms turns arbitrary content into truth. A successful cryptographic check establishes that particular bytes have not changed and that the operation was performed with the corresponding key. The application must still establish who owns that key, whether the algorithm is acceptable, and whether this token was intended for the current endpoint.",[11,7669,7670,7671,673,7674,673,7677,673,7680,7683],{},"A safe validator does not choose an algorithm merely because an untrusted header names it. It starts with an allowlist from local configuration, binds the key to an expected issuer, and verifies the cryptographic operation. It then evaluates ",[49,7672,7673],{},"iss",[49,7675,7676],{},"aud",[49,7678,7679],{},"exp",[49,7681,7682],{},"nbf",", and whatever other claims the surrounding protocol requires. ID Tokens, access tokens, and other JWTs also need distinct validation rules so that a correctly signed object of one type cannot be substituted where another is expected.",[15,7685,7687],{"id":7686},"oauth-does-not-require-jwt-while-openid-connect-requires-it-for-id-tokens","OAuth does not require JWT, while OpenID Connect requires it for ID Tokens",[11,7689,7690,7691,7696],{},"JWT frequently appears alongside OAuth 2.0, but the terms are not interchangeable. OAuth defines delegated access without imposing a single access-token format. An authorization server can issue an opaque string that a resource server checks through storage or introspection, or a self-contained JWT that the resource server validates locally. ",[38,7692,7695],{"href":7693,"rel":7694},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9068.html",[429],"RFC 9068"," standardizes a profile for the latter option without removing the former.",[11,7698,7699],{},"An OAuth client should treat an access token as opaque even when the current value happens to look like a JWT. Its format is a contract between the authorization server and resource server and may change; client behavior should not depend on decoding access-token claims.",[11,7701,7702,7707],{},[38,7703,7706],{"href":7704,"rel":7705},"https:\u002F\u002Fopenid.net\u002Fspecs\u002Fopenid-connect-core-1_0-18.html",[429],"OpenID Connect"," draws a clearer boundary. The ID Token is required to be a JWT and tells the client about the user's authentication event. The access token is for calling an API and may use a different format, while the refresh token obtains new access tokens and is not required to be a JWT either. An ID Token must not be sent to an API in place of an access token merely because the two strings can look similar.",[15,7709,7711],{"id":7710},"jwt-cookies-and-server-sessions-are-not-opposing-choices","JWT, cookies, and server sessions are not opposing choices",[11,7713,7714,7715,7718],{},"A cookie describes how a browser stores a small value and attaches it automatically to matching requests. JWT defines a claims structure. A server-side session means that significant state remains on the server while the client usually holds a random identifier. These mechanisms occupy different layers: a JWT can travel in an ",[49,7716,7717],{},"Authorization"," header or a cookie, and a cookie can contain either a JWT or a session ID.",[11,7720,7721],{},"Local JWT validation lets a resource server obtain claims without querying a central store, but it does not guarantee a stateless system. Immediate revocation, current authorization checks, key rotation, and replay detection can still require shared state. An opaque identifier adds a server or network lookup but makes central revocation easier and avoids disclosing claims to the client.",[11,7723,7724,7725,673,7728,7731,7732,7735],{},"There is no universal browser storage location for every token. A Backend for Frontend can keep OAuth tokens on the server and give the browser a protected ",[49,7726,7727],{},"HttpOnly",[49,7729,7730],{},"Secure",", and appropriately configured ",[49,7733,7734],{},"SameSite"," session cookie. JavaScript cannot read that cookie, but the backend still needs CSRF defenses, and XSS can issue requests as the user while malicious code remains active on the page.",[11,7737,7738,7739,7741,7742,7747],{},"When a browser application receives an access token directly, memory storage limits its lifetime but does not survive a reload. ",[49,7740,1455],{}," is convenient for persistence, yet any script executing in the same origin after an XSS compromise can read it. The current ",[38,7743,7746],{"href":7744,"rel":7745},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc10017.html",[429],"RFC 10017"," evaluates these designs against specific attacker capabilities instead of declaring one storage API safe for every application.",[15,7749,7751],{"id":7750},"stealing-a-bearer-token-is-dangerous-regardless-of-its-format","Stealing a bearer token is dangerous regardless of its format",[11,7753,7754],{},"An ordinary OAuth access token is a bearer credential, so whoever possesses it can present it to a resource server. JWT versus opaque encoding does not change that property. Short lifetimes, minimum scope, precise audience restrictions, TLS, protected storage, and revocation all reduce the impact of a leak. Sender-constrained tokens additionally require proof of a key, so copying the token string alone is not sufficient for replay.",[11,7756,7757,7758,7763],{},"A refresh token normally lives longer and can mint new access tokens, making its loss more serious. ",[38,7759,7762],{"href":7760,"rel":7761},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9700.html",[429],"RFC 9700"," requires refresh-token rotation or sender constraint for public clients. These controls belong to OAuth security and do not follow automatically from using JWT internally.",[15,7765,7767],{"id":7766},"what-jwt-inspector-checks","What JWT Inspector checks",[11,7769,510,7770,7774,7775,673,7778,673,7780,1228,7782,7785],{},[38,7771,7773],{"href":7772},"\u002Ftools\u002Fjwt-inspector","JWT Inspector"," runs locally in the browser and is intended for compact JWS values with three segments. It decodes the JSON header and payload, renders numeric ",[49,7776,7777],{},"iat",[49,7779,7679],{},[49,7781,7682],{},[49,7783,7784],{},"auth_time"," claims as dates, and separately marks a token whose expiration has passed or whose not-before time is still in the future.",[11,7787,7788,7789,7791],{},"For MAC verification, the tool accepts a plain or Base64URL secret and supports HS256, HS384, and HS512. For asymmetric verification, it accepts a public PEM key and supports RS256\u002F384\u002F512, PS256\u002F384\u002F512, and ES256\u002F384\u002F512. Web Crypto performs every operation locally; the token and entered key are not sent to a server. The tool does not decrypt JWE, and it identifies ",[49,7790,7650],{}," as an unsigned mode that cannot be verified.",[11,7793,7794,7795,673,7797,7799,7800,7802],{},"The tool's result is not full token validation. Decoding establishes no trust, while a successful signature result proves a match only with the key supplied by the user. Inspector does not retrieve a trusted key through metadata or JWKS and does not validate ",[49,7796,7673],{},[49,7798,7676],{},", token type, scope, or application policy. Even a green ",[49,7801,7679],{}," badge is based on one time claim and cannot turn the input into an acceptable access token or ID Token.",[11,7804,7805],{},"For a meaningful test, use a non-production token, select a key from a trusted source, and define the expected algorithm, issuer, audience, and purpose beforehand. Inspector helps reveal the structure and check the cryptographic operation in isolation; the validator for the surrounding protocol must make the final acceptance decision.",[15,7807,358],{"id":357},[357,7809],{":slug":7810},"[\"jwt-inspector\",\"hash-generator\",\"key-pair-generator\",\"webhook-verifier\"]",{"title":363,"searchDepth":364,"depth":364,"links":7812},[7813,7814,7815,7816,7817,7818,7819,7820],{"id":7597,"depth":364,"text":7598},{"id":7616,"depth":364,"text":7617},{"id":7660,"depth":364,"text":7661},{"id":7686,"depth":364,"text":7687},{"id":7710,"depth":364,"text":7711},{"id":7750,"depth":364,"text":7751},{"id":7766,"depth":364,"text":7767},{"id":357,"depth":364,"text":358},"2026-08-02","Understand JWT, JWS, and JWE, the role of tokens in OAuth and OpenID Connect, and the difference between decoding, signature checks, and validation.",[7824,7827,7830,7833,7836],{"q":7825,"a":7826},"Is a JWT always signed but never encrypted?","No. JWT describes a claims set that can be protected as a JWS or encrypted as a JWE. Developers most often encounter a signed or MACed compact JWS with three segments, where the header and payload are readable without a key. Compact JWE has five segments and places the claims inside its ciphertext. The standard also permits an unsecured JWT with alg=none, but an application should accept that mode only in an explicitly defined closed context.",{"q":7828,"a":7829},"How do decoding, signature verification, and validation differ?","Decoding only turns Base64URL into a readable header and claims. Verifying a signature or MAC establishes that the protected bytes have not changed and were processed by the holder of the relevant key. Full validation must also enforce an allowed algorithm, a trusted source for the key, issuer, audience, time limits, token type, and the rules of the receiving application.",{"q":7831,"a":7832},"Is an OAuth access token always a JWT?","No. OAuth does not require one access-token format: a token can be an opaque random string or a JWT. RFC 9068 defines a profile for JWT access tokens, but clients should not build behavior around reading their claims even then. In OpenID Connect, the ID Token is required to be a JWT because it carries information about the authentication event to the client.",{"q":7834,"a":7835},"Does JWT replace server sessions and cookies?","Not necessarily. JWT is a data format, a cookie is a browser storage and transport mechanism, and a session describes how an application manages state. A JWT can be stored in a cookie, while a cookie can instead carry a random server-side session identifier. The choice between locally validated tokens and server-managed state depends on revocation, scaling, and the threat model.",{"q":7837,"a":7838},"What happens when a JWT access token is stolen?","If it is an ordinary bearer access token, possession is enough to call the API whether its format is JWT or opaque. Short lifetimes, narrow scope and audience, protected storage, and revocation reduce the impact; some systems also bind a token to a sender key. Refresh tokens need stronger protection and generally use rotation or sender constraint.","\u002Fimages\u002Fblog\u002Fblog_jwt-history.webp",{},"\u002Fblog\u002Fen\u002Fjwt-history",{"title":7582,"description":7822},"blog\u002Fen\u002Fjwt-history",[7845,7846,7847,7848,7849,7850],"What is JWT","JWT vs session","Is JWT secure","How JWT tokens work","Base64 is not encryption","JWS vs JWE","MDZahNnXW5J6s6fo6BaJGB1Gj7LjOt7x49wjomJ-2jY",{"id":7853,"title":7854,"body":7855,"date":7988,"description":7989,"extension":376,"faq":7990,"image":8003,"lang":394,"meta":8004,"navigation":396,"path":8005,"published":396,"readTime":1591,"seo":8006,"stem":8007,"tags":8008,"updated":405,"__hash__":8014},"blog\u002Fblog\u002Fen\u002Fuuid-history.md","How UUIDv7 Made Time Part of UUID Ordering",{"type":8,"value":7856,"toc":7980},[7857,7860,7864,7871,7880,7889,7893,7900,7903,7906,7910,7921,7924,7936,7942,7946,7955,7958,7962,7972,7975,7977],[11,7858,7859],{},"UUID was not invented as a prettier way to write an identifier. It answered a practical distributed-systems problem: different machines needed to create unique values independently, without asking one central service for the next number. That idea survived several platforms and standards, but the most popular version, UUIDv4, left databases with the awkward insertion pattern of a fully random key. UUIDv7 keeps decentralized generation while moving time to the front of the value, which usually places identifiers created near each other close together when they are sorted.",[15,7861,7863],{"id":7862},"from-apollo-ncs-to-an-ietf-standard","From Apollo NCS to an IETF standard",[11,7865,7866,7867,7870],{},"The ancestor of the modern UUID appeared in ",[26,7868,7869],{},"Apollo Computer's Network Computing System",". Objects in a distributed system needed identifiers that could be issued on different computers without central coordination. The Open Software Foundation later carried this approach into its Distributed Computing Environment, which is why older documentation still uses the name DCE UUID. Microsoft adapted the same general format under the name GUID for COM and Windows.",[11,7872,7873,7874,7879],{},"In 2005, ",[38,7875,7878],{"href":7876,"rel":7877},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc4122",[429],"RFC 4122"," collected widely used UUID variants in one IETF document. Version 1 combined time, a clock sequence, and a 48-bit node field that could hold a network interface address or a randomized replacement. Version 2 belonged to DCE Security, versions 3 and 5 deterministically derived a value from a namespace and name using MD5 or SHA-1, and UUIDv4 devoted 122 bits to random data. Version 4 became the common choice for public identifiers because it is easy to generate and does not encode a timestamp or device information.",[11,7881,7882,7883,7888],{},"In May 2024, ",[38,7884,7887],{"href":7885,"rel":7886},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9562",[429],"RFC 9562"," replaced RFC 4122, clarified the older formats, and standardized versions 6, 7, and 8. UUIDv6 rearranges the fields of UUIDv1 so time comes first, UUIDv8 reserves room for application-specific schemes, and UUIDv7 uses a modern Unix timestamp without inheriting the field layout of v1.",[15,7890,7892],{"id":7891},"why-a-random-key-makes-b-tree-work-less-local","Why a random key makes B-tree work less local",[11,7894,7895,7896,7899],{},"The statement that a database stores rows in primary-key order is not true for every system. PostgreSQL normally keeps rows in a heap table and creates a separate unique B-tree for a ",[49,7897,7898],{},"PRIMARY KEY",". InnoDB, by contrast, uses the primary key as a clustered index that contains the row data. Despite that architectural difference, the index in either system must locate the page for a new key and add an entry there.",[11,7901,7902],{},"UUIDv4 spreads inserts evenly across the full index range. As an index grows, that can enlarge the working set of pages, reduce cache locality, and split full pages in many parts of the tree. UUIDv7 directs values created around the same time into a neighboring B-tree range, so inserts usually touch fewer pages. This is a useful property, not a universal performance guarantee. The result depends on the database engine, index size, fill factor, storage, and workload. Under heavy concurrency, sequential keys can also create contention at the right edge of an index.",[11,7904,7905],{},"An auto-incrementing key is not inherently unsafe or badly designed. It is compact, simple, and often an excellent internal key for a single database. The drawback appears when the sequence is exposed publicly and reveals an approximate record count, or when several nodes must create identifiers without coordinating with one sequence. UUIDv7 is useful in those cases because it combines decentralized generation with a more index-friendly order.",[15,7907,7909],{"id":7908},"how-uuidv7-is-laid-out","How UUIDv7 is laid out",[11,7911,7912,7913,7916,7917,7920],{},"The first 48 bits of UUIDv7 contain the number of milliseconds since the Unix epoch. They are followed by four version bits, a 12-bit ",[49,7914,7915],{},"rand_a"," field, two variant bits, and a 62-bit ",[49,7918,7919],{},"rand_b"," field. In a typical implementation, the last two random fields provide 74 random bits in total. The RFC also permits an implementation to use some of that space for a sub-millisecond fraction or a monotonic counter, but that is an optional generation strategy rather than a mandatory part of the format.",[11,7922,7923],{},"Because the timestamp occupies the most significant bits, a UUIDv7 from a later millisecond compares as a larger 128-bit value. This provides a useful approximate chronology, but it does not promise a total ordering of events. If several identifiers are created in one millisecond and the remaining bits are random, their order relative to one another is random as well. A clock moving backward can also break the sequence between milliseconds unless the implementation handles that case explicitly.",[11,7925,7926,7927,7935],{},"Generation support depends on the database version. PostgreSQL 18, for example, added a built-in ",[38,7928,7931,7934],{"href":7929,"rel":7930},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002F18\u002Ffunctions-uuid.html",[429],[49,7932,7933],{},"uuidv7()"," function"," and timestamp extraction for UUIDv7. If a database can store UUID values but cannot generate version 7 itself, the application can create the identifier and store it as the same 128-bit type. Check the documentation for the exact database version before relying on a native function.",[11,7937,7938],{},[1068,7939],{"alt":7940,"src":7941},"Exact RFC 9562 UUIDv7 layout with a 48-bit timestamp, version and variant fields, 74 random bits, and a separate optional monotonic scheme","\u002Fimages\u002Fblog\u002Finline_uuid-history.webp",[15,7943,7945],{"id":7944},"how-uuidv7-differs-from-ulid-nanoid-and-cuid2","How UUIDv7 differs from ULID, NanoID, and CUID2",[11,7947,7948,7949,7954],{},"The canonical ",[38,7950,7953],{"href":7951,"rel":7952},"https:\u002F\u002Fgithub.com\u002Fulid\u002Fspec",[429],"ULID specification"," also puts a 48-bit Unix millisecond timestamp first, then encodes it with 80 random bits as 26 characters of Crockford Base32. The alphabet and timestamp position make ULID strings lexicographically sortable. Basic random generation does not guarantee order within the same millisecond, although a separate monotonic API can increment the random portion for that purpose.",[11,7956,7957],{},"NanoID and CUID2 address a different problem. They offer compact or hard-to-guess identifiers, but a leading timestamp is not a required property of either format, so they should not be described as direct sortable alternatives to UUIDv7. The right choice depends on whether a project values a standard database UUID type, chronological locality, shorter text, or compatibility with a particular library.",[15,7959,7961],{"id":7960},"what-our-generator-creates","What our generator creates",[11,7963,510,7964,7968,7969,7971],{},[38,7965,7967],{"href":7966},"\u002Ftools\u002Fuuid-generator","UUID Generator"," creates UUIDv4, UUIDv7, and UUIDv1 with a randomized node instead of a real MAC address. It also offers ULID, NanoID, and a SHA-256-based CUID2-like identifier. The last option follows the broad idea rather than calling the official CUID2 library. The tool uses ",[49,7970,5504],{}," for random data, supports batch generation, and can change the textual representation of its output.",[11,7973,7974],{},"Our UUIDv7 implementation fills all 74 available bits with random data. It does not add a same-millisecond counter, so several values generated almost simultaneously are not guaranteed to preserve issue order. To inspect the time component, select UUIDv7, generate a few values with a short pause between them, and compare their leading characters. UUIDv4 will show no equivalent pattern because it contains no timestamp.",[15,7976,358],{"id":357},[357,7978],{":slug":7979},"[\"uuid-generator\",\"hash-generator\",\"key-pair-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":7981},[7982,7983,7984,7985,7986,7987],{"id":7862,"depth":364,"text":7863},{"id":7891,"depth":364,"text":7892},{"id":7908,"depth":364,"text":7909},{"id":7944,"depth":364,"text":7945},{"id":7960,"depth":364,"text":7961},{"id":357,"depth":364,"text":358},"2026-07-31","The history of UUID from Apollo NCS to RFC 9562, how UUIDv7 works, how it differs from UUIDv4, and where time ordering helps a database.",[7991,7994,7997,8000],{"q":7992,"a":7993},"Why is UUIDv7 better than UUIDv4?","UUIDv7 puts time in the most significant bits, so identifiers from different milliseconds sort roughly in creation order. When UUIDs are used as indexed keys, that usually gives inserts better locality than fully random UUIDv4 values. UUIDv7 does not guarantee strict ordering within one millisecond unless the generator adds a counter or another monotonic scheme.",{"q":7995,"a":7996},"Does UUIDv1 always expose the computer's MAC address?","No. In the classic design, the node field could contain an IEEE network address, so some UUIDv1 values did reveal information about their source device. The specification also permits a random or pseudorandom node with the multicast bit set. Our generator uses a randomized node and does not read the computer's MAC address.",{"q":7998,"a":7999},"Are ULID and UUIDv7 the same format?","No, although both formats begin with a 48-bit Unix millisecond timestamp. ULID uses 26 characters of Crockford Base32 and keeps 80 bits for randomness, while UUIDv7 preserves the standard 128-bit UUID layout with version and variant fields. Both need a monotonic implementation if strict ordering within one millisecond is required.",{"q":8001,"a":8002},"Can two random UUIDs collide?","A collision is theoretically possible. UUIDv4 has 122 random bits, and the birthday bound puts the probability of at least one collision near 50% after roughly 2.7×10^18 values. That does not remove the need for a sound random number generator, but it makes collisions negligible at ordinary application volumes.","\u002Fimages\u002Fblog\u002Fblog_uuid-history.webp",{},"\u002Fblog\u002Fen\u002Fuuid-history",{"title":7854,"description":7989},"blog\u002Fen\u002Fuuid-history",[8009,8010,7887,8011,8012,8013],"UUID history","what is UUIDv7","ULID vs UUID","unique identifier","sortable database id","b0oFpCBOydVKx4jtKljEi_bKlCBfwJO8NJZi0DuVlzU",{"id":8016,"title":8017,"body":8018,"date":8218,"description":8219,"extension":376,"faq":8220,"image":8236,"lang":394,"meta":8237,"navigation":396,"path":8238,"published":396,"readTime":1270,"seo":8239,"stem":8240,"tags":8241,"updated":405,"__hash__":8247},"blog\u002Fblog\u002Fen\u002Fdocker-toolbox.md","Docker Toolbox: How Docker Made Containers Practical",{"type":8,"value":8019,"toc":8210},[8020,8023,8027,8037,8052,8067,8076,8082,8086,8089,8098,8102,8119,8138,8147,8150,8154,8167,8173,8177,8191,8198,8205,8207],[11,8021,8022],{},"Docker is often credited with bringing containers into mainstream software development, although the isolation mechanisms themselves are much older. Its defining contribution was not a new way to restrict a process. Docker connected application builds, dependencies, image distribution, and one-command execution into a coherent workflow, turning a collection of operating-system facilities into something an application developer could use directly.",[15,8024,8026],{"id":8025},"containers-existed-long-before-docker","Containers existed long before Docker",[11,8028,8029,8030,8033,8034,8036],{},"One early ancestor was ",[49,8031,8032],{},"chroot",", included in Version 7 Unix in 1979. It changes the root directory visible to a process and can separate one filesystem view from another, but it does not isolate every process, network resource, or other part of the host. That makes ",[49,8035,8032],{}," a useful building block rather than a complete security boundary.",[11,8038,8039,8040,8045,8046,8051],{},"Later systems extended isolation beyond the filesystem. ",[38,8041,8044],{"href":8042,"rel":8043},"https:\u002F\u002Fdocs.freebsd.org\u002Fen\u002Fbooks\u002Fhandbook\u002Fjails\u002F",[429],"Jails",", introduced with FreeBSD 4.0, provided separate views of users, processes, and networking. Solaris Zones developed a similar idea into a managed form of ",[38,8047,8050],{"href":8048,"rel":8049},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Foperating-systems\u002Fsolaris\u002Foracle-solaris\u002F11.4\u002Fvirt-intro\u002Foverview-oracle-solaris-11.4-virtualization-environments.html",[429],"operating-system virtualization",". Unlike a virtual machine, these environments do not boot an independent kernel. Their processes continue to share the host kernel, which usually reduces overhead but also ties their compatibility and security model to that common kernel. Modern containers retain the same fundamental tradeoff.",[11,8053,8054,8055,8060,8061,8066],{},"On Linux, a container emerged from several kernel mechanisms developed over multiple releases rather than from one container switch. ",[38,8056,8059],{"href":8057,"rel":8058},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fnamespaces.7.html",[429],"Namespaces"," give a group of processes a private view of process IDs, mount points, networks, users, and other system resources. ",[38,8062,8065],{"href":8063,"rel":8064},"https:\u002F\u002Fdocs.kernel.org\u002Fadmin-guide\u002Fcgroup-v1\u002Fcgroups.html",[429],"Control groups",", usually called cgroups, organize processes hierarchically and let the system account for or limit resources such as memory and CPU time. Namespaces primarily answer “what can this process see?”, while cgroups answer “how much may it use?” A practical container also needs capabilities, networking rules, a filesystem, and additional security controls.",[11,8068,8069,8070,8075],{},"By the end of the 2000s, ",[38,8071,8074],{"href":8072,"rel":8073},"https:\u002F\u002Flinuxcontainers.org\u002Flxc\u002Fintroduction\u002F",[429],"LXC"," had assembled these Linux features into user-space libraries and commands for creating containers. It was already a capable and useful toolkit, so describing it as merely “almost Docker” misses the distinction. LXC was principally designed to create an environment resembling a separate Linux system. Docker would instead focus its workflow on packaging and delivering a particular application.",[11,8077,8078],{},[1068,8079],{"alt":8080,"src":8081},"The evolution of operating system isolation from chroot, FreeBSD Jails, and Solaris Zones to cgroups, namespaces, and Docker containers","\u002Fimages\u002Fblog\u002Finline_docker-toolbox.webp",[15,8083,8085],{"id":8084},"the-problem-inside-dotcloud","The problem inside dotCloud",[11,8087,8088],{},"Solomon Hykes founded dotCloud to build a platform as a service that could run customer applications written in different languages with different dependency stacks. Isolation solved only part of that platform's problem. The team also needed a repeatable way to accept an application, construct its environment, move the resulting artifact between machines, and start it consistently without requiring every customer to understand LXC or host configuration.",[11,8090,8091,8092,8097],{},"The team extracted the solution it had developed inside dotCloud into an open-source project. On ",[38,8093,8096],{"href":8094,"rel":8095},"https:\u002F\u002Fwww.docker.com\u002Fblog\u002Fdocker-nine-years-young\u002F",[429],"March 15, 2013",", Hykes gave the first public Docker demonstration at PyCon and framed the problem in practical terms: shipping code to a server was hard for developers. Early Docker releases used LXC as their execution environment, but Docker was more than a friendlier command wrapper around it. The project introduced its own image model and a common interface connecting an application's build, storage, transfer, and execution.",[15,8099,8101],{"id":8100},"what-docker-actually-changed","What Docker actually changed",[11,8103,8104,8105,1121,8108,8111,8112,1121,8115,8118],{},"Docker's usability came from the combination of several ideas. A Dockerfile put the environment recipe beside the application code so that a build could be repeated instead of reproduced by hand on a server. Image layers allowed unchanged work to be reused and cached. A registry gave images a shared address, making ",[49,8106,8107],{},"push",[49,8109,8110],{},"pull"," a natural way to move the same artifact between a laptop, CI, and a server. Finally, commands such as ",[49,8113,8114],{},"docker build",[49,8116,8117],{},"docker run"," hid much of the underlying system configuration behind a short and reasonably consistent interface.",[11,8120,8121,8122,8127,8128,8133,8134,8137],{},"These pieces did not all arrive in the first public demo. Docker's release history records the Dockerfile builder in ",[38,8123,8126],{"href":8124,"rel":8125},"https:\u002F\u002Fdocs.docker.com\u002Fengine\u002Frelease-notes\u002Fprior-releases\u002F",[429],"version 0.4 in June 2013",", while the central registry and support for running a private registry were evolving during the same year. Docker Compose grew out of ",[38,8129,8132],{"href":8130,"rel":8131},"https:\u002F\u002Fwww.docker.com\u002Fblog\u002Fkitematic-a-docker-gui-joins-the-docker-family\u002F",[429],"the separate Fig project"," and joined the ecosystem later, so ",[49,8135,8136],{},"docker compose up"," should not be counted among Docker's original 2013 innovations.",[11,8139,8140,8141,8146],{},"The implementation underneath that workflow changed as well. In March 2014, Docker 0.9 made ",[38,8142,8145],{"href":8143,"rel":8144},"https:\u002F\u002Fwww.docker.com\u002Fblog\u002Fdocker-0-9-introducing-execution-drivers-and-libcontainer\u002F",[429],"libcontainer"," its default execution driver. The new library worked directly with Linux namespaces, cgroups, capabilities, and networking instead of requiring LXC. This transition illustrates why Docker and containers are not synonyms: the user-facing build and run workflow survived even when the underlying runtime implementation was replaced.",[11,8148,8149],{},"The practical result mattered more than any single command. A developer had a transportable artifact with explicit dependencies, CI could test the same image that would be placed in a registry, and a server could start it from the same instructions. This did not erase differences in CPU architecture, kernel configuration, secrets, storage, or networking, so “runs anywhere” has always come with conditions. It did, however, remove many of the accidental differences between environments.",[15,8151,8153],{"id":8152},"from-one-product-to-shared-standards","From one product to shared standards",[11,8155,8156,8157,8162,8163,8166],{},"As the container ecosystem expanded, tying image formats and runtime behavior to one vendor became a risk. In June 2015, Docker, CoreOS, and other companies launched the ",[38,8158,8161],{"href":8159,"rel":8160},"https:\u002F\u002Fopencontainers.org\u002Fabout\u002Foverview\u002F",[429],"Open Container Initiative"," under the Linux Foundation. Docker contributed its container format and ",[49,8164,8165],{},"runC",", and the effort developed open runtime, image, and distribution specifications.",[11,8168,8169,8170,8172],{},"OCI does not make every container tool interchangeable. Docker, containerd, Podman, CRI-O, and orchestrators still address different layers of the problem and provide different features. The specifications define compatible boundaries between an image, its distribution, and its runtime, allowing that image to outlive any one implementation. Docker's lasting influence therefore reaches beyond the popularity of ",[49,8171,8117],{},": it helped establish the container image as a portable unit of application delivery.",[15,8174,8176],{"id":8175},"what-docker-toolbox-does","What Docker Toolbox does",[11,8178,8179,8183,8184,8186,8187,8190],{},[38,8180,8182],{"href":8181},"\u002Ftools\u002Fdocker-toolbox","Docker Toolbox"," does not build images. Its primary mode parses an existing ",[49,8185,8117],{}," command and converts supported options into a ",[49,8188,8189],{},"docker-compose.yml"," service. The converter currently understands the container name and image, published ports, mounted volumes, environment variables, and the restart policy.",[11,8192,8193,8194,8197],{},"The second mode is a compact maintenance reference. It provides commands for pruning unused Docker resources, stopping and removing all containers, opening a shell in a selected container, and following recent log output. Cleanup commands deserve a careful review before execution: ",[49,8195,8196],{},"docker system prune -a --volumes",", for example, removes unused images, networks, and volumes in addition to stopped containers.",[11,8199,8200,8201,8204],{},"For a quick conversion, paste a command such as ",[49,8202,8203],{},"docker run -d --name web -p 8080:80 -e NODE_ENV=production nginx:alpine",". The tool generates a Compose service carrying over the image, port, name, and environment variable. You can copy that result and then add networks, dependencies, or any settings that were not represented by the original command.",[15,8206,358],{"id":357},[357,8208],{":slug":8209},"[\"docker-toolbox\",\"nginx-builder\",\"systemd-builder\",\"yaml-json-converter\"]",{"title":363,"searchDepth":364,"depth":364,"links":8211},[8212,8213,8214,8215,8216,8217],{"id":8025,"depth":364,"text":8026},{"id":8084,"depth":364,"text":8085},{"id":8100,"depth":364,"text":8101},{"id":8152,"depth":364,"text":8153},{"id":8175,"depth":364,"text":8176},{"id":357,"depth":364,"text":358},"2026-07-30","Docker did not invent containers, but combined process isolation, reproducible images, a registry, and a clear interface into one application delivery workflow.",[8221,8224,8227,8230,8233],{"q":8222,"a":8223},"Who created Docker?","Docker was created by Solomon Hykes and the team at dotCloud. The project was first demonstrated publicly at PyCon on March 15, 2013, then developed in the open with a growing community of contributors.",{"q":8225,"a":8226},"How is a container different from a VM?","A virtual machine runs a separate guest operating system with its own kernel. A conventional Linux container shares the host kernel and relies on kernel facilities to isolate its processes. Containers therefore tend to start faster and use fewer resources, but their isolation boundary is different from that of a virtual machine.",{"q":8228,"a":8229},"What is the history of containerization?","Modern containers were preceded by Unix chroot, FreeBSD Jails, Solaris Zones, and the Linux namespaces and cgroups mechanisms. LXC assembled Linux kernel facilities into a user-space toolkit, while Docker added an application-focused workflow for building, storing, transferring, and running images.",{"q":8231,"a":8232},"What is Docker Compose?","Docker Compose describes a multi-container application in YAML, including its services, networks, volumes, and runtime settings. The docker compose up command creates and starts that set as one application. Compose arrived after Docker's initial release and was not part of the feature set demonstrated in 2013.",{"q":8234,"a":8235},"What is the Open Container Initiative (OCI)?","The Open Container Initiative was established under the Linux Foundation in 2015 to create open standards for container images, runtimes, and distribution. These specifications allow different compatible tools and runtimes to understand the same image.","\u002Fimages\u002Fblog\u002Fblog_docker-toolbox.webp",{},"\u002Fblog\u002Fen\u002Fdocker-toolbox",{"title":8017,"description":8219},"blog\u002Fen\u002Fdocker-toolbox",[8242,8243,8182,8244,8245,8246],"Docker","Solomon Hykes","containerization","Dockerfile","Docker Compose","n873_1aowC7GZ4tlADPbY-uWEQ4RBGjGqgqtsfHm_ac",{"id":8249,"title":8250,"body":8251,"date":8430,"description":8431,"extension":376,"faq":8432,"image":8442,"lang":394,"meta":8443,"navigation":396,"path":8444,"published":396,"readTime":364,"seo":8445,"stem":8446,"tags":8447,"updated":405,"__hash__":8454},"blog\u002Fblog\u002Fen\u002Fdns-toolbox.md","DNS-over-HTTPS (DoH) & Email Security (SPF, DMARC)",{"type":8,"value":8252,"toc":8420},[8253,8256,8260,8263,8270,8284,8288,8291,8295,8301,8307,8311,8318,8324,8328,8343,8349,8353,8361,8406,8415,8417],[11,8254,8255],{},"The Domain Name System (DNS) is the fundamental directory of the internet. Every second, millions of servers query A, MX, and TXT records. In recent years, DNS has evolved from plaintext UDP\u002F53 packets to encrypted DNS-over-HTTPS (DoH), while email security now demands a strict three-tier defense: SPF, DMARC, and DKIM.",[15,8257,8259],{"id":8258},"the-evolution-of-dns-from-udp53-to-doh","The Evolution of DNS: From UDP\u002F53 to DoH",[11,8261,8262],{},"Historically, DNS queries were sent unencrypted over UDP port 53. Any intermediary router could intercept, log, or spoof DNS responses.",[11,8264,8265,8266,8269],{},"With ",[26,8267,8268],{},"DoH (RFC 8484)",", published in October 2018 by Paul Hoffman (ICANN) and Patrick McManus (Mozilla), DNS queries are encapsulated inside standard HTTPS requests over TLS. This offers two major benefits:",[4001,8271,8272,8278],{},[301,8273,8274,8277],{},[26,8275,8276],{},"Privacy & Tamper Resistance",": Responses are signed by trusted DoH resolvers (Cloudflare 1.1.1.1, Google 8.8.8.8).",[301,8279,8280,8283],{},[26,8281,8282],{},"Browser Native Queries",": Client-side web tools can inspect DNS records without relying on backend proxy servers.",[15,8285,8287],{"id":8286},"email-authentication-triad-spf-dmarc-and-dkim","Email Authentication Triad: SPF, DMARC, and DKIM",[11,8289,8290],{},"Major email providers (Google Workspace, Microsoft 365) enforce strict authentication requirements:",[70,8292,8294],{"id":8293},"_1-spf-sender-policy-framework","1. SPF (Sender Policy Framework)",[11,8296,8297,8300],{},[26,8298,8299],{},"Meng Weng Wong"," combined two earlier anti-spoofing proposals — Hadmut Danisch's \"Reverse MX\" and Gordon Fecyk's \"Designated Mailer Protocol,\" both inspired by a 2002 paper from BIND author Paul Vixie — into SPF in mid-2003, announcing it on a mailing list that June. It became RFC 4408 in 2006 and was refined into the current RFC 7208 in 2014. A TXT record lists authorized sending IP addresses and email providers:",[1564,8302,8305],{"className":8303,"code":8304,"language":4815,"meta":363},[4813],"v=spf1 a mx include:_spf.google.com include:sendgrid.net ~all\n",[49,8306,8304],{"__ignoreMap":363},[70,8308,8310],{"id":8309},"_2-dmarc-domain-based-message-authentication","2. DMARC (Domain-based Message Authentication)",[11,8312,8313,8314,8317],{},"DMARC started as an internal PayPal initiative around 2010 — the company was a favorite phishing target and pushed a coalition of Google, Microsoft, Yahoo, and others to standardize a shared policy layer on top of SPF and DKIM. The spec went public in 2011 and became RFC 7489 in March 2015, authored by Murray Kucherawy and Elizabeth Zwicky. A TXT record at ",[49,8315,8316],{},"_dmarc.yourdomain.com"," dictates how receiver servers handle unauthenticated emails:",[1564,8319,8322],{"className":8320,"code":8321,"language":4815,"meta":363},[4813],"v=DMARC1; p=quarantine; sp=reject; rua=mailto:dmarc@yourdomain.com; pct=100\n",[49,8323,8321],{"__ignoreMap":363},[70,8325,8327],{"id":8326},"_3-dkim-domainkeys-identified-mail","3. DKIM (DomainKeys Identified Mail)",[11,8329,8330,8331,8334,8335,8338,8339,8342],{},"DKIM merged two competing 2004 proposals: Yahoo's ",[26,8332,8333],{},"DomainKeys",", built by Mark Delany, and Cisco's ",[26,8336,8337],{},"Identified Internet Mail",", built by Jim Fenton and Michael Thomas. The combined standard shipped as RFC 4871 in May 2007 — Yahoo granting a royalty-free patent license the same year removed the last obstacle to adoption — and was consolidated into the current RFC 6376 in 2011. A TXT record contains a public cryptographic key at ",[49,8340,8341],{},"selector._domainkey.yourdomain.com",":",[1564,8344,8347],{"className":8345,"code":8346,"language":4815,"meta":363},[4813],"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ...\n",[49,8348,8346],{"__ignoreMap":363},[15,8350,8352],{"id":8351},"features-of-our-dns-spfdmarc-inspector","Features of Our DNS & SPF\u002FDMARC Inspector",[11,8354,8355,8356,8360],{},"Our ",[38,8357,8359],{"href":8358},"\u002Ftools\u002Fdns-toolbox","DNS Lookup & SPF\u002FDMARC Inspector"," brings live testing and rule generation into a single interface:",[298,8362,8363,8369,8375,8400],{},[301,8364,8365,8368],{},[26,8366,8367],{},"Live DoH Dig",": Query A, AAAA, MX, TXT, NS, CNAME, and CAA records via Cloudflare and Google DoH APIs.",[301,8370,8371,8374],{},[26,8372,8373],{},"Interactive SPF Builder",": One-click provider chips for Google Workspace, M365, SendGrid, and Mailgun.",[301,8376,8377,8380,8381,673,8384,8387,8388,8391,8392,8395,8396,8399],{},[26,8378,8379],{},"DMARC & DKIM Constructor",": Customize alignment (",[49,8382,8383],{},"aspf",[49,8385,8386],{},"adkim","), filtering percentages (",[49,8389,8390],{},"pct","), and reporting mailboxes (",[49,8393,8394],{},"rua","\u002F",[49,8397,8398],{},"ruf",").",[301,8401,8402,8405],{},[26,8403,8404],{},"BIND Zone Exporter",": Export formatted DNS zone files for authoritative BIND server deployments.",[11,8407,8408,8410,8411,8414],{},[26,8409,353],{}," open the tool → query your domain with Live DoH Query → switch to SPF Builder and toggle your mail providers → switch to DMARC Builder and start with ",[49,8412,8413],{},"p=none"," to monitor before enforcing → publish both TXT records → re-run the lookup to confirm they resolve.",[15,8416,6614],{"id":357},[357,8418],{":slug":8419},"[\"dns-toolbox\",\"ssl-decoder\",\"subnet-calculator\",\"command-explainer\"]",{"title":363,"searchDepth":364,"depth":364,"links":8421},[8422,8423,8428,8429],{"id":8258,"depth":364,"text":8259},{"id":8286,"depth":364,"text":8287,"children":8424},[8425,8426,8427],{"id":8293,"depth":370,"text":8294},{"id":8309,"depth":370,"text":8310},{"id":8326,"depth":370,"text":8327},{"id":8351,"depth":364,"text":8352},{"id":357,"depth":364,"text":6614},"2026-07-29","From legacy UDP DNS queries to DNS-over-HTTPS (DoH), understanding email authentication (SPF, DMARC, DKIM), and generating BIND zone files.",[8433,8436,8439],{"q":8434,"a":8435},"What is DNS-over-HTTPS (DoH) and why is it better?","DoH (RFC 8484) encrypts DNS lookups over HTTPS (port 443\u002FTLS). Unlike traditional unencrypted UDP\u002F53 queries, DoH prevents MITM eavesdropping, DNS spoofing, and provider tampering, while enabling web applications to execute DNS queries directly in the browser.",{"q":8437,"a":8438},"What is the difference between SPF, DMARC, and DKIM?","SPF defines which IP addresses and services are authorized to send email for your domain. DKIM adds a cryptographic signature to outgoing email headers. DMARC tells receiving mail servers what policy to enforce (none, quarantine, reject) when SPF or DKIM checks fail.",{"q":8440,"a":8441},"What is the difference between ~all and -all in SPF?","~all (SoftFail) advises receiving mail servers to accept the email but mark it as suspicious. -all (HardFail) strictly instructs receiving servers to reject non-compliant messages.","\u002Fimages\u002Fblog\u002Fblog_dns-toolbox.webp",{},"\u002Fblog\u002Fen\u002Fdns-toolbox",{"title":8250,"description":8431},"blog\u002Fen\u002Fdns-toolbox",[8448,8449,8450,8451,8452,8453],"DNS over HTTPS","DoH lookup tool","SPF record generator","DMARC policy builder","DKIM record builder","BIND zone file","DP6G83bPdfzNE9EbiukknxJi4Ov7X6IxjuI0gqHOxCg",{"id":8456,"title":8457,"body":8458,"date":8430,"description":9318,"extension":376,"faq":9319,"image":9341,"lang":394,"meta":9342,"navigation":396,"path":9343,"published":396,"readTime":742,"seo":9344,"stem":9345,"tags":9346,"updated":405,"__hash__":9353},"blog\u002Fblog\u002Fen\u002Fpixel-painter.md","How I Made Pixel Art Without Opening Photoshop",{"type":8,"value":8459,"toc":9302},[8460,8463,8466,8470,8477,8488,8491,8495,8501,8531,8534,8537,8544,8548,8551,8594,8601,8621,8627,8631,8634,8743,8746,8750,8757,8770,8777,8784,8788,8791,8814,8817,8820,8858,8869,8873,8876,8890,8893,8896,8900,8907,8921,8937,8940,8944,8950,8982,8985,8989,8992,8995,8998,9002,9008,9011,9024,9027,9044,9047,9060,9066,9070,9255,9259,9262,9294,9297,9299],[11,8461,8462],{},"Some tasks do not justify opening Photoshop — like driving a micro-nail with a sledgehammer. A 16×16 pixel icon is one of them.",[11,8464,8465],{},"I wanted a tool that opens immediately, lets me redraw an icon pixel by pixel, and exports a clean SVG or PNG without installation, accounts, or uploads. That small utility gradually became a proper compact editor.",[15,8467,8469],{"id":8468},"what-pixel-painter-is","What Pixel Painter is",[11,8471,8472,8476],{},[38,8473,8475],{"href":8474},"\u002Ftools\u002Fpixel-painter","Pixel Painter"," is a single-layer pixel art editor that runs entirely in the browser. It can be used in three ways:",[298,8478,8479,8482,8485],{},[301,8480,8481],{},"Start with a blank canvas and draw an icon from scratch.",[301,8483,8484],{},"Open a raster image as a reference, generate a pixel draft, and clean it up manually.",[301,8486,8487],{},"Import an SVG as an editable pixel grid, modify it, and export or save it again.",[11,8489,8490],{},"The editor deliberately has no traditional layer stack. There is one editable pixel layer, an optional source image underneath it, and a temporary floating selection used when pasting a copied block.",[15,8492,8494],{"id":8493},"opening-images-and-svg-files","Opening images and SVG files",[11,8496,510,8497,8500],{},[26,8498,8499],{},"File"," section supports several input workflows:",[298,8502,8503,8509,8515,8528],{},[301,8504,8505,8508],{},[26,8506,8507],{},"Image"," opens PNG, WebP, JPEG, and other raster formats supported by the browser.",[301,8510,8511,8514],{},[26,8512,8513],{},"SVG"," opens an existing SVG file and converts its visible content into editable cells.",[301,8516,8517,8520,8521,8524,8525,507],{},[26,8518,8519],{},"From text"," accepts a complete ",[49,8522,8523],{},"\u003Csvg>",", an SVG fragment, or raw path data beginning with ",[49,8526,8527],{},"M",[301,8529,8530],{},"A blank canvas can be created simply by entering its width and height.",[11,8532,8533],{},"An image or SVG can also be dragged directly onto the central editor. If the pixel layer already contains artwork, Pixel Painter asks for confirmation before replacing it. Cancelling the prompt leaves the current document untouched, while an accepted replacement remains available through Undo.",[11,8535,8536],{},"SVG input is analyzed before import. The editor reports invalid source and warnings, determines the visible bounds, rasterizes the result to a grid up to 256×256 cells, and builds a working palette from the most frequently used colors.",[11,8538,8539,8540,8543],{},"An imported SVG automatically switches export to ",[26,8541,8542],{},"Full canvas",", so its original position and surrounding transparent space are preserved by default.",[15,8545,8547],{"id":8546},"turning-an-image-into-editable-pixel-art","Turning an image into editable pixel art",[11,8549,8550],{},"A raster image is kept as a reference background rather than being mixed into the editable layer. Pixel Painter then provides a short conversion workflow:",[4001,8552,8553,8560,8563,8566,8585,8591],{},[301,8554,8555,8556,8559],{},"It tries to detect a suitable grid size automatically. You can rerun detection with ",[26,8557,8558],{},"Auto"," or set the width and height manually.",[301,8561,8562],{},"It extracts suggested palettes containing between 2 and 12 colors.",[301,8564,8565],{},"You choose a working or brand palette.",[301,8567,8568,8569,8572,8573,8576,8577,8580,8581,8584],{},"Select the ",[26,8570,8571],{},"Dither"," mode: ",[1321,8574,8575],{},"None"," (solid colors), ",[1321,8578,8579],{},"Bayer 2×2"," (retro grid), or ",[1321,8582,8583],{},"Subtle"," (soft noise).",[301,8586,8587,8590],{},[26,8588,8589],{},"Generate"," maps every visible source cell to the nearest color in that palette.",[301,8592,8593],{},"You refine the generated draft with the normal drawing tools.",[11,8595,8596,8597,8600],{},"If the raster reference image does not align neatly with the pixel grid, you can refine it in the ",[26,8598,8599],{},"Background adjust"," section:",[298,8602,8603,8609,8615],{},[301,8604,8605,8608],{},[26,8606,8607],{},"Offset X and Y"," shift the reference image in sub-pixel increments of 0.25 cells (or exact numeric values) to align cut pixels with grid borders.",[301,8610,8611,8614],{},[26,8612,8613],{},"Scale"," adjusts the background scale from 10% to 300% with a 5% stepper or range slider.",[301,8616,8617,8618,8620],{},"Background rendering runs at a smooth 60 FPS, while color extraction for the eyedropper and the ",[26,8619,8589],{}," draft tool automatically respects the updated background coordinates.",[11,8622,510,8623,8626],{},[26,8624,8625],{},"Auto-regenerate on grid change"," option can rebuild the draft whenever the grid changes. The source image can also be hidden, faded with the background-opacity control, compared side by side with the editor, or removed after the draft is ready.",[15,8628,8630],{"id":8629},"drawing-tools-and-vector-primitives","Drawing tools and vector primitives",[11,8632,8633],{},"The left toolbar houses core drawing and shape tools:",[298,8635,8636,8648,8656,8668,8680,8686,8695,8704,8717,8725,8734],{},[301,8637,8638,8644,8645,8647],{},[26,8639,8640,8641,219],{},"Pencil (",[49,8642,8643],{},"B"," — paints with selected color or transparency. Brush size ranges from 1×1 to 4×4 cells. Holding ",[49,8646,6136],{}," draws a straight line or 45-degree diagonal.",[301,8649,8650,8655],{},[26,8651,8652,8653,219],{},"Line (",[49,8654,5688],{}," — draws a clean line segment between two points with real-time outline preview.",[301,8657,8658,8664,8665,8667],{},[26,8659,8660,8661,219],{},"Rect (",[49,8662,8663],{},"R"," — draws a rectangle diagonally from initial click. Holding ",[49,8666,6136],{}," locks aspect ratio to a square.",[301,8669,8670,8676,8677,8679],{},[26,8671,8672,8673,219],{},"Ellipse (",[49,8674,8675],{},"U"," — constructs an ellipse inside a bounding box. Holding ",[49,8678,6136],{}," produces a perfect pixel circle.",[301,8681,8682,8685],{},[26,8683,8684],{},"Symmetry (mirror)"," — toggles horizontal (↔), vertical (↕), or 4-way (✚) mirror drawing across all brushes and shape primitives.",[301,8687,8688,8694],{},[26,8689,8690,8691,219],{},"Eraser (",[49,8692,8693],{},"E"," — erases cells with selected brush size.",[301,8696,8697,8703],{},[26,8698,8699,8700,219],{},"Fill (",[49,8701,8702],{},"G"," — flood-fills connected area with selected color or transparency.",[301,8705,8706,8712,8713,8716],{},[26,8707,8708,8709,219],{},"Eyedropper (",[49,8710,8711],{},"I"," — picks color from pixel layer or reference image. Temporary eyedropper opens via ",[49,8714,8715],{},"Alt","-click.",[301,8718,8719,8724],{},[26,8720,8721,8722,219],{},"Select (",[49,8723,8527],{}," — creates a rectangular editable selection.",[301,8726,8727,8733],{},[26,8728,8729,8730,219],{},"Move (",[49,8731,8732],{},"V"," — moves active selection, or whole pixel layer if no selection exists.",[301,8735,8736,8742],{},[26,8737,8738,8739,219],{},"Crop (",[49,8740,8741],{},"C"," — crops canvas to an adjustable rectangular boundary.",[11,8744,8745],{},"When symmetry is active, dashed orange axis guide lines appear on the canvas. At higher zoom levels, the canvas also highlights center guide axes and 8-cell major grid subdivisions. The current cell coordinates, canvas dimensions, visible artwork bounds, painted-cell count, and zoom are shown in the status bar.",[15,8747,8749],{"id":8748},"transparent-drawing-and-preview-backgrounds","Transparent drawing and preview backgrounds",[11,8751,8752,8753,8756],{},"Transparency is a first-class paint mode, not just an eraser action. In the ",[26,8754,8755],{},"Color"," section you can switch between:",[298,8758,8759,8764],{},[301,8760,8761,8763],{},[26,8762,8755],{}," — Pencil and Fill add the current color.",[301,8765,8766,8769],{},[26,8767,8768],{},"Transparent"," — Pencil and Fill remove cells.",[11,8771,8772,8773,8776],{},"Selecting a slot in the working palette reveals a ",[26,8774,8775],{},"Recolor"," button: with one click, it replaces all pixels of that color on the canvas with the currently active paint color (with Undo support).",[11,8778,8779,8780,8783],{},"The separate, collapsible ",[26,8781,8782],{},"Preview background"," section shows how transparent artwork will look on checkerboard, white, light, dark, black, or custom backgrounds. Grid lines automatically adapt contrast (light grid for dark backgrounds) for maximum cell readability.",[15,8785,8787],{"id":8786},"selections-movement-cloning-and-transforms","Selections, movement, cloning, and transforms",[11,8789,8790],{},"The selection tools provide a lightweight substitute for object editing in a single-layer editor:",[4001,8792,8793,8801,8804,8811],{},[301,8794,8795,8796,8800],{},"Choose ",[26,8797,8721,8798,219],{},[49,8799,8527],{}," and drag a rectangle.",[301,8802,8803],{},"Move the frame or adjust its sides and corners with the handles.",[301,8805,8795,8806,8810],{},[26,8807,8729,8808,219],{},[49,8809,8732],{}," to move the selected pixels with the mouse.",[301,8812,8813],{},"Use the arrow keys to nudge them exactly one cell at a time.",[11,8815,8816],{},"When the Move tool is active without a selection, the same mouse and arrow controls move the whole layer. Pixels pushed outside the canvas are removed, and the operation can be undone.",[11,8818,8819],{},"You can also clone a block:",[298,8821,8822,8828,8835,8838,8847],{},[301,8823,8824,8825,507],{},"Copy the selection with ",[49,8826,8827],{},"Ctrl\u002FCmd+C",[301,8829,8830,8831,8834],{},"Paste it with ",[49,8832,8833],{},"Ctrl\u002FCmd+V"," as a floating copy.",[301,8836,8837],{},"Move the copy with the mouse or arrow keys.",[301,8839,8840,8841,698,8844,507],{},"Reflect it with ",[26,8842,8843],{},"Flip H",[26,8845,8846],{},"Flip V",[301,8848,8849,8850,8853,8854,8857],{},"Press ",[49,8851,8852],{},"Enter"," or click ",[26,8855,8856],{},"Place"," to merge it into the pixel layer.",[11,8859,8860,8861,8864,8865,8868],{},"The same horizontal and vertical flip buttons work directly on an ordinary selection. ",[49,8862,8863],{},"Ctrl\u002FCmd+D"," removes the selection frame; this shortcut is used instead of ",[49,8866,8867],{},"Escape",", so macOS does not leave full-screen mode when you only want to deselect.",[15,8870,8872],{"id":8871},"colors-and-palettes","Colors and palettes",[11,8874,8875],{},"Colors can be selected with the native color picker or entered as three- or six-digit hex values. Pixel Painter maintains two palette types:",[298,8877,8878,8884],{},[301,8879,8880,8883],{},[26,8881,8882],{},"Working palette"," — usually extracted from the imported image or SVG and used during editing.",[301,8885,8886,8889],{},[26,8887,8888],{},"Brand colors"," — a reusable palette stored in the browser for future sessions.",[11,8891,8892],{},"Brand colors can be added, removed, reset, copied from the working palette, edited one per line, or loaded from the built-in design-system library. The list can be copied to the clipboard, and its format is compatible with the Color Toolbox workflow.",[11,8894,8895],{},"A working-palette slot can also be edited with the eyedropper. When its color changes, all already-painted cells using the old color are recolored together, with Undo support.",[15,8897,8899],{"id":8898},"canvas-size-crop-and-alignment","Canvas size, crop, and alignment",[11,8901,8902,8903,8906],{},"Canvas dimensions are independent and can be adjusted from 1 to 256 cells in either direction. For more controlled changes, ",[26,8904,8905],{},"Resize"," offers:",[298,8908,8909,8912,8915,8918],{},[301,8910,8911],{},"exact width and height;",[301,8913,8914],{},"a locked aspect ratio, enabled by default;",[301,8916,8917],{},"relative resizing, such as adding several rows or columns;",[301,8919,8920],{},"a nine-point anchor that determines where existing artwork stays when the canvas changes.",[11,8922,510,8923,8926,8927,8930,8931,8933,8934,8936],{},[26,8924,8925],{},"Center"," command aligns the visible artwork in the canvas. ",[26,8928,8929],{},"Crop"," uses a movable, resizable frame; press ",[49,8932,8852],{}," to apply it or ",[49,8935,8867],{}," to cancel it. Crop and resize include the source background and can be undone.",[11,8938,8939],{},"The Canvas section also transforms the complete document: rotate left or right by 90 degrees, or flip horizontally or vertically. These actions transform both the pixel layer and the loaded reference image. A 32×16 canvas becomes 16×32 after rotation, and every action is available through Undo.",[15,8941,8943],{"id":8942},"viewing-and-navigation","Viewing and navigation",[11,8945,510,8946,8949],{},[26,8947,8948],{},"View"," controls let you:",[298,8951,8952,8959,8965,8972,8975],{},[301,8953,8954,8955,8958],{},"show or hide the source background (",[49,8956,8957],{},"H",") and adjust its opacity;",[301,8960,8961,8962,8958],{},"show or hide the editable layer (",[49,8963,8964],{},"T",[301,8966,8967,8968,8971],{},"compare the editor with the original side by side (",[49,8969,8970],{},"O",");",[301,8973,8974],{},"zoom with buttons or the mouse wheel;",[301,8976,8977,8978,8981],{},"hold ",[49,8979,8980],{},"Space"," and drag to pan around a large or highly zoomed canvas.",[11,8983,8984],{},"The cell grid appears automatically at a useful zoom level. Sidebar sections are collapsible, and their open or closed state is remembered by the browser.",[15,8986,8988],{"id":8987},"undo-redo-clearing-and-autosave","Undo, redo, clearing, and autosave",[11,8990,8991],{},"Drawing, filling, moving, selection and full-canvas transforms, palette recoloring, cropping, resizing, centering, and clearing the canvas participate in Undo\u002FRedo history. The trash button clears the pixel layer without making the action irreversible.",[11,8993,8994],{},"Pixel Painter also autosaves the current canvas, source image, colors, working palette, opacity settings, preview background, export settings, and other editor state to local browser storage. Reloading the page restores the working session when storage is available.",[11,8996,8997],{},"Nothing is sent to a server.",[15,8999,9001],{"id":9000},"svg-and-png-export","SVG and PNG export",[11,9003,510,9004,9007],{},[26,9005,9006],{},"Export"," section controls the output filename, region, and pixel dimensions.",[11,9009,9010],{},"Two region modes are available:",[298,9012,9013,9019],{},[301,9014,9015,9018],{},[26,9016,9017],{},"Trim to content"," removes unused transparent space around the artwork.",[301,9020,9021,9023],{},[26,9022,8542],{}," preserves the canvas dimensions and the artwork's position inside it.",[11,9025,9026],{},"You can then:",[298,9028,9029,9032,9035,9038,9041],{},[301,9030,9031],{},"download a compact SVG with crisp pixel edges;",[301,9033,9034],{},"download a nearest-neighbor PNG with transparency;",[301,9036,9037],{},"copy the generated SVG source directly to the clipboard;",[301,9039,9040],{},"set an exact output width while preserving the artwork's proportions;",[301,9042,9043],{},"leave the size empty to omit explicit SVG dimensions and export PNG at 10 pixels per cell.",[11,9045,9046],{},"Adjacent cells of the same color are merged into compact SVG paths, keeping the result much smaller than one rectangle per pixel.",[11,9048,9049,9050,9052,9053,1228,9056,9059],{},"If the browser supports direct file access, an SVG opened with the ",[26,9051,8513],{}," button keeps its file handle. The Export section then shows ",[26,9054,9055],{},"Save current SVG",[49,9057,9058],{},"Ctrl\u002FCmd+S"," overwrites the opened file after permission is granted. Browsers without this API still support normal SVG download.",[11,9061,9062,9065],{},[26,9063,9064],{},"Open in SVG Optimizer"," sends the generated SVG to the companion optimizer through one-time browser session storage. There you can minify or prettify it, inspect it on another background, apply SVG transforms, and export React, React Native, Vue, PNG, or Data URI. The transfer stays inside the browser and is removed after the optimizer reads it.",[15,9067,9069],{"id":9068},"keyboard-shortcuts","Keyboard shortcuts",[75,9071,9072,9082],{},[78,9073,9074],{},[81,9075,9076,9079],{},[84,9077,9078],{},"Shortcut",[84,9080,9081],{},"Action",[91,9083,9084,9099,9109,9122,9136,9146,9156,9164,9175,9184,9197,9206,9215,9228,9238,9246],{},[81,9085,9086,9096],{},[96,9087,9088,673,9090,673,9092,673,9094],{},[49,9089,8643],{},[49,9091,8693],{},[49,9093,8702],{},[49,9095,8711],{},[96,9097,9098],{},"Pencil, Eraser, Fill, Eyedropper",[81,9100,9101,9106],{},[96,9102,9103],{},[49,9104,9105],{},"S",[96,9107,9108],{},"Toggle symmetry mode (Off → H → V → 4-Way)",[81,9110,9111,9119],{},[96,9112,9113,673,9115,673,9117],{},[49,9114,8527],{},[49,9116,8732],{},[49,9118,8741],{},[96,9120,9121],{},"Select, Move, Crop",[81,9123,9124,9133],{},[96,9125,9126,9129,9130],{},[49,9127,9128],{},"1","–",[49,9131,9132],{},"4",[96,9134,9135],{},"Brush size",[81,9137,9138,9143],{},[96,9139,9140,9142],{},[49,9141,6136],{}," while drawing",[96,9144,9145],{},"Straight or 45-degree line",[81,9147,9148,9153],{},[96,9149,9150,9152],{},[49,9151,8715],{},"-click",[96,9154,9155],{},"Temporarily pick a color",[81,9157,9158,9161],{},[96,9159,9160],{},"Arrow keys",[96,9162,9163],{},"Nudge the active frame, selection, copy, or layer by one cell",[81,9165,9166,9172],{},[96,9167,9168,673,9170],{},[49,9169,8827],{},[49,9171,8833],{},[96,9173,9174],{},"Copy and paste a selected block",[81,9176,9177,9181],{},[96,9178,9179],{},[49,9180,8863],{},[96,9182,9183],{},"Deselect",[81,9185,9186,9194],{},[96,9187,9188,673,9191],{},[49,9189,9190],{},"Ctrl\u002FCmd+Z",[49,9192,9193],{},"Ctrl\u002FCmd+Shift+Z",[96,9195,9196],{},"Undo and redo",[81,9198,9199,9203],{},[96,9200,9201],{},[49,9202,8852],{},[96,9204,9205],{},"Place a floating copy or apply Crop",[81,9207,9208,9212],{},[96,9209,9210],{},[49,9211,9058],{},[96,9213,9214],{},"Overwrite the opened SVG when supported",[81,9216,9217,9225],{},[96,9218,9219,673,9221,673,9223],{},[49,9220,8957],{},[49,9222,8964],{},[49,9224,8970],{},[96,9226,9227],{},"Background, layer, and comparison view",[81,9229,9230,9235],{},[96,9231,9232,9234],{},[49,9233,8980],{}," + drag",[96,9236,9237],{},"Pan the canvas",[81,9239,9240,9243],{},[96,9241,9242],{},"Mouse wheel",[96,9244,9245],{},"Zoom",[81,9247,9248,9252],{},[96,9249,9250],{},[49,9251,6735],{},[96,9253,9254],{},"Open the shortcut reference",[15,9256,9258],{"id":9257},"a-practical-workflow","A practical workflow",[11,9260,9261],{},"For a logo or icon, the quickest route is:",[4001,9263,9264,9267,9270,9273,9276,9279,9282,9285],{},[301,9265,9266],{},"Open a source image or SVG.",[301,9268,9269],{},"Check the automatically detected grid and adjust it if necessary.",[301,9271,9272],{},"Choose a suggested palette or load your brand colors.",[301,9274,9275],{},"Generate a draft.",[301,9277,9278],{},"Hide or fade the source and clean the result with Pencil, Fill, transparency, and selections.",[301,9280,9281],{},"Crop, resize, center, rotate, or flip the finished icon.",[301,9283,9284],{},"Check it on several preview backgrounds.",[301,9286,9287,9288,9290,9291,9293],{},"Export ",[26,9289,9017],{}," for a tight standalone icon, use ",[26,9292,8542],{}," when spacing and position matter, or continue in SVG Optimizer for framework code and additional formats.",[11,9295,9296],{},"That is the point of Pixel Painter: enough editing power to finish a real icon, without turning a small pixel task into a full graphics-software project.",[15,9298,358],{"id":357},[357,9300],{":slug":9301},"[\"pixel-painter\",\"image-optimizer\",\"color-toolbox\",\"qr-generator\"]",{"title":363,"searchDepth":364,"depth":364,"links":9303},[9304,9305,9306,9307,9308,9309,9310,9311,9312,9313,9314,9315,9316,9317],{"id":8468,"depth":364,"text":8469},{"id":8493,"depth":364,"text":8494},{"id":8546,"depth":364,"text":8547},{"id":8629,"depth":364,"text":8630},{"id":8748,"depth":364,"text":8749},{"id":8786,"depth":364,"text":8787},{"id":8871,"depth":364,"text":8872},{"id":8898,"depth":364,"text":8899},{"id":8942,"depth":364,"text":8943},{"id":8987,"depth":364,"text":8988},{"id":9000,"depth":364,"text":9001},{"id":9068,"depth":364,"text":9069},{"id":9257,"depth":364,"text":9258},{"id":357,"depth":364,"text":358},"Pixel Painter is a local browser pixel art editor: draw from scratch, pixelize PNG, WebP, or SVG, edit selections and palettes, and export SVG or PNG.",[9320,9323,9326,9329,9332,9335,9338],{"q":9321,"a":9322},"What is Pixel Painter?","Pixel Painter is a browser-based pixel art editor. You can draw from scratch, turn a raster image or SVG into an editable pixel grid, work with colors and selections, and export the result as SVG or PNG. Everything runs locally in your browser.",{"q":9324,"a":9325},"Which files can Pixel Painter open?","It can open PNG, WebP, JPEG, and other image formats supported by the browser, as well as SVG files. You can also paste a complete SVG, an SVG fragment, or raw SVG path data into the text importer.",{"q":9327,"a":9328},"What drawing and editing tools are available?","Pencil with 1×1 to 4×4 brushes, symmetry\u002Fmirror modes (horizontal, vertical, 4-way), straight and 45-degree lines, eraser, flood fill, eyedropper, transparent paint, sub-pixel background adjustment (offset & scale), draft dithering (Bayer\u002FSubtle), quick canvas color replacement (Recolor), selection, move, copy and paste, selection flip, full-canvas rotate and flip, crop, canvas resize, center guide axes, undo, and redo.",{"q":9330,"a":9331},"Can I move or clone part of an image?","Yes. Select a rectangular block, move it with the mouse or arrow keys, or copy and paste it as a floating clone. The clone can be moved and flipped before you place it.",{"q":9333,"a":9334},"Can Pixel Painter preserve transparency?","Yes. Transparent cells remain transparent in SVG and PNG. You can erase them, draw with transparency, flood-fill an area with transparency, and preview the artwork on checkerboard, white, light, dark, black, or custom backgrounds. Preview backgrounds are never exported.",{"q":9336,"a":9337},"Can I overwrite an SVG after editing it?","Yes, in browsers that support direct file access. Open the SVG through the SVG file picker, edit it, then use Save current SVG or Ctrl\u002FCmd+S. In other browsers you can still export a new SVG file.",{"q":9339,"a":9340},"Does Pixel Painter upload my images?","No. Editing, conversion, autosave, and export happen locally in the browser. Your working session and palettes are stored in local browser storage.","\u002Fimages\u002Fblog\u002Fblog_pixel-painter.webp",{},"\u002Fblog\u002Fen\u002Fpixel-painter",{"title":8457,"description":9318},"blog\u002Fen\u002Fpixel-painter",[9347,9348,9349,9350,9351,9352],"pixel art","PNG to SVG","raster to vector","pixel art editor","browser graphics","SVG from PNG","qOjanwUdqV4wNJl7Cvl9iyuZ4zVlXxyurOsI91vPmbQ",{"id":9355,"title":9356,"body":9357,"date":10191,"description":10192,"extension":376,"faq":10193,"image":10218,"lang":394,"meta":10219,"navigation":396,"path":10220,"published":396,"readTime":4389,"seo":10221,"stem":10222,"tags":10223,"updated":405,"__hash__":10227},"blog\u002Fblog\u002Fen\u002Fvector-painter.md","Vector Painter: Trace Raster Icons into Editable SVG",{"type":8,"value":9358,"toc":10176},[9359,9362,9365,9369,9376,9404,9407,9411,9416,9436,9439,9443,9450,9523,9526,9530,9533,9652,9656,9659,9715,9719,9726,9738,9741,9745,9751,9765,9772,9776,9782,9825,9829,9838,9844,9848,9853,9903,9906,10110,10114,10168,10171,10173],[11,9360,9361],{},"Opening heavy desktop software like Illustrator, Figma, or Inkscape just to fix a single small icon is often overkill. When you need to convert a pixelated PNG logo into a clean vector, adjust slightly off Bézier handles, or recolor an icon before dropping it into your code, you want an instant tool right inside your web browser.",[11,9363,9364],{},"Automatic raster conversion rarely yields perfect results on the first attempt. Tracing creates a foundation, but the geometry still demands smooth handles, proper node types, noise cleanup, and precise palette control.",[15,9366,9368],{"id":9367},"what-is-vector-painter","What is Vector Painter",[11,9370,9371,9375],{},[38,9372,9374],{"href":9373},"\u002Ftools\u002Fvector-painter","Vector Painter"," is a local browser-based vector tracer and path editor. It solves three primary developer tasks:",[298,9377,9378,9388,9394],{},[301,9379,9380,9383,9384,9387],{},[26,9381,9382],{},"Automated Raster Tracing:"," Converts PNG, WebP, and JPEG images into clean vector ",[49,9385,9386],{},"path"," elements.",[301,9389,9390,9393],{},[26,9391,9392],{},"Manual Geometry Editing:"," Fine-tunes anchor points, Bézier control handles, scaling, and object transformations.",[301,9395,9396,9399,9400,9403],{},[26,9397,9398],{},"Vector Creation from Scratch:"," Builds icons and illustrations using the Pen tool (",[49,9401,9402],{},"P",") and shape primitives.",[11,9405,9406],{},"All operations execute 100% locally. The editor never uploads your files to external servers and automatically saves your working session in your browser's local storage.",[15,9408,9410],{"id":9409},"importing-images-svg-and-code","Importing Images, SVG, and Code",[11,9412,510,9413,9415],{},[26,9414,8499],{}," section supports multiple convenient input methods:",[298,9417,9418,9423,9428],{},[301,9419,9420,9422],{},[26,9421,8507],{}," — Opens raster files (PNG, WebP, JPEG) for automated vector tracing or reference overlay.",[301,9424,9425,9427],{},[26,9426,8513],{}," — Loads existing SVG files and converts visible vector geometry into editable objects.",[301,9429,9430,9432,9433,507],{},[26,9431,8519],{}," — Accepts raw SVG code, XML fragments, or path data attribute strings starting with ",[49,9434,9435],{},"d=\"M...\"",[11,9437,9438],{},"Files can also be dropped directly onto the canvas area via drag-and-drop. Re-importing asks for confirmation, and any accidental replacement can be reversed using Undo.",[15,9440,9442],{"id":9441},"how-raster-to-vector-tracing-works","How Raster-to-Vector Tracing Works",[11,9444,9445,9446,9449],{},"When you load a raster image, enabling the ",[26,9447,9448],{},"Trace"," panel triggers the vectorization pipeline:",[4001,9451,9452,9458,9468,9492],{},[301,9453,9454,9457],{},[26,9455,9456],{},"Color Quantization (1–12 Colors):"," Set the target color count. The engine automatically clusters close color values.",[301,9459,9460,9463,9464,9467],{},[26,9461,9462],{},"Background Separation:"," The ",[1321,9465,9466],{},"Separate background"," option detects background colors (like white or transparent) so palette budgets aren't wasted on outer margins.",[301,9469,9470,9473],{},[26,9471,9472],{},"Tracing Mode Selection:",[298,9474,9475,9481,9487],{},[301,9476,9477,9480],{},[26,9478,9479],{},"Filled shapes"," — Builds outline contours around solid silhouettes and logo shapes.",[301,9482,9483,9486],{},[26,9484,9485],{},"Centerlines"," — Analyzes stroke thickness and generates thin central vector paths, ideal for line art and technical sketches.",[301,9488,9489,9491],{},[26,9490,8558],{}," — Automatically selects the optimal algorithm based on image contrast and structure.",[301,9493,9494,9497],{},[26,9495,9496],{},"Contour Fine-Tuning:",[298,9498,9499,9505,9511,9517],{},[301,9500,9501,9504],{},[26,9502,9503],{},"Smoothness"," — Controls the smoothing of stair-stepped pixel edges.",[301,9506,9507,9510],{},[26,9508,9509],{},"Corner threshold"," — Preserves sharp corners and prevents unwanted rounding at sharp angles.",[301,9512,9513,9516],{},[26,9514,9515],{},"Max stroke width"," (for Centerlines) — Restricts maximum recognized stroke thickness.",[301,9518,9519,9522],{},[26,9520,9521],{},"Min area filter"," — Filters out tiny pixel noise and unwanted specks.",[11,9524,9525],{},"Results instantly render as vector objects on your canvas, ready for further manual editing.",[15,9527,9529],{"id":9528},"vector-editor-and-bézier-curves","Vector Editor and Bézier Curves",[11,9531,9532],{},"After tracing or when drawing from scratch, you have a complete set of vector editing tools:",[298,9534,9535,9545,9584,9609,9645],{},[301,9536,9537,9544],{},[26,9538,8721,9539,9541,9542,219],{},[49,9540,8732],{}," \u002F ",[49,9543,9105],{}," — Selects and moves shapes. Transforms objects, resizes via bounding box handles, and rotates shapes.",[301,9546,9547,9555,9556],{},[26,9548,9549,9550,9541,9553,219],{},"Edit nodes (",[49,9551,9552],{},"A",[49,9554,1080],{}," — Direct path node editing tool:\n",[298,9557,9558,9561,9566,9571,9574],{},[301,9559,9560],{},"Dragging a point shifts its canvas position.",[301,9562,9563,9565],{},[49,9564,8715],{}," + dragging a point pulls out symmetrical Bézier control handles.",[301,9567,9568,9570],{},[49,9569,8715],{}," + dragging an individual handle breaks handle symmetry to create sharp corner nodes.",[301,9572,9573],{},"Clicking a path segment adds a new anchor point.",[301,9575,9576,9577,9541,9580,9583],{},"Pressing ",[49,9578,9579],{},"Delete",[49,9581,9582],{},"Backspace"," on a selected node (or double-clicking a node) deletes it from the path string — automatically reconnecting adjacent nodes with a smooth recalculated Bézier join without breaking shape continuity.",[301,9585,9586,9591,9592],{},[26,9587,9588,9589,219],{},"Pen (",[49,9590,9402],{}," — Classic Pen tool for custom vector paths:\n",[298,9593,9594,9597,9600,9603],{},[301,9595,9596],{},"Single click creates a sharp corner node.",[301,9598,9599],{},"Click-and-drag creates a smooth node with Bézier handles.",[301,9601,9602],{},"Clicking the initial starting point closes the path.",[301,9604,9605,9606,9608],{},"Double-clicking or pressing ",[49,9607,8852],{}," finishes an open path.",[301,9610,9611,9614,9615,9619,9620,9625,9626,9631,9632,9636,9637,9641,9642,9644],{},[26,9612,9613],{},"Shape Primitives:"," Rectangle ",[26,9616,8660,9617,219],{},[49,9618,8663],{},", Rounded Rectangle ",[26,9621,9622,9623,219],{},"Roundrect (",[49,9624,8675],{},", Circle ",[26,9627,9628,9629,219],{},"Circle (",[49,9630,8741],{},", Ellipse ",[26,9633,8672,9634,219],{},[49,9635,8693],{},", and Straight Line ",[26,9638,8652,9639,219],{},[49,9640,5688],{}," (holding ",[49,9643,6136],{}," snaps line angles to 45° increments).",[301,9646,9647,9651],{},[26,9648,8708,9649,219],{},[49,9650,8711],{}," — Picks colors from both reference raster images and existing vector shapes.",[15,9653,9655],{"id":9654},"layer-ordering-and-object-manipulation","Layer Ordering and Object Manipulation",[11,9657,9658],{},"Vector objects are managed within an object stack. You can easily manage Z-index layering:",[298,9660,9661,9670,9679,9688,9697,9705],{},[301,9662,9663,9669],{},[26,9664,9665,9666,219],{},"Bring Forward (",[49,9667,9668],{},"Ctrl\u002FCmd+]"," — Move selected object up one layer.",[301,9671,9672,9678],{},[26,9673,9674,9675,219],{},"Send Backward (",[49,9676,9677],{},"Ctrl\u002FCmd+["," — Move selected object down one layer.",[301,9680,9681,9687],{},[26,9682,9683,9684,219],{},"Bring to Front (",[49,9685,9686],{},"Ctrl\u002FCmd+Shift+]"," — Move selected object to the top of the stack.",[301,9689,9690,9696],{},[26,9691,9692,9693,219],{},"Send to Back (",[49,9694,9695],{},"Ctrl\u002FCmd+Shift+["," — Move selected object to the bottom of the stack.",[301,9698,9699,9704],{},[26,9700,9701,9702,219],{},"Duplicate (",[49,9703,8863],{}," — Clone the selected shape instantly.",[301,9706,9707,9714],{},[26,9708,9709,9710,9541,9712,219],{},"Delete (",[49,9711,9579],{},[49,9713,9582],{}," — Remove the selected object or node.",[15,9716,9718],{"id":9717},"brand-colors-and-one-click-recoloring","Brand Colors and One-Click Recoloring",[11,9720,9721,9722,9725],{},"Vector Painter utilizes the unified ",[26,9723,9724],{},"Brand Palette"," system:",[298,9727,9728,9733],{},[301,9729,9730,9732],{},[26,9731,8882],{}," — Automatically extracted from imported images or tracing results.",[301,9734,9735,9737],{},[26,9736,8888],{}," — Your personal brand palette library, persisted across browser sessions.",[11,9739,9740],{},"You can import presets from built-in design systems, edit hex codes in bulk, or copy color lists to your clipboard. Clicking any palette color with an object selected immediately recolors its fill or stroke.",[15,9742,9744],{"id":9743},"preview-backgrounds-and-transparency","Preview Backgrounds and Transparency",[11,9746,9747,9748,9750],{},"To evaluate icon contrast across different UI themes, the ",[26,9749,8782],{}," section offers multiple background modes:",[298,9752,9753,9756,9759,9762],{},[301,9754,9755],{},"Checkered grid (for transparency verification);",[301,9757,9758],{},"Pure white or light gray;",[301,9760,9761],{},"Dark gray or pure black;",[301,9763,9764],{},"Custom color (via color picker or hex input).",[11,9766,9767,9768,9771],{},"Preview backgrounds are strictly for display and are ",[26,9769,9770],{},"never included"," in exported SVG or PNG output.",[15,9773,9775],{"id":9774},"navigation-and-subpixel-reference-alignment","Navigation and Subpixel Reference Alignment",[11,9777,9778,9779,9781],{},"To perfectly align vector paths over reference raster images, the ",[26,9780,8599],{}," panel provides fine controls:",[298,9783,9784,9790,9795],{},[301,9785,9786,9789],{},[26,9787,9788],{},"Offset X & Y"," — Shift background reference image with 0.25px subpixel precision.",[301,9791,9792,9794],{},[26,9793,8613],{}," — Scale reference image from 10% to 300% in 5% increments.",[301,9796,9797,9800],{},[26,9798,9799],{},"Navigation & View Shortcuts:",[298,9801,9802,9807,9812,9817,9822],{},[301,9803,9804,9806],{},[49,9805,8957],{}," — Toggle reference image visibility.",[301,9808,9809,9811],{},[49,9810,8964],{}," — Toggle vector path layer visibility.",[301,9813,9814,9816],{},[49,9815,8970],{}," — Toggle side-by-side comparison mode between original image and vector overlay.",[301,9818,9819,9821],{},[49,9820,8980],{}," + drag — Pan canvas at high zoom levels.",[301,9823,9824],{},"Mouse wheel — Smooth zoom in\u002Fout.",[15,9826,9828],{"id":9827},"undo-redo-and-local-persistence","Undo, Redo, and Local Persistence",[11,9830,9831,9832,9834,9835,9837],{},"Every action — tracing, adding shapes, moving Bézier nodes, modifying handles, recoloring, and aligning — is recorded in the Undo (",[49,9833,9190],{},") \u002F Redo (",[49,9836,9193],{},") history stack.",[11,9839,9840,9841,9843],{},"Document state, active paths, raster reference images, selected palettes, and zoom levels automatically persist in ",[49,9842,1455],{},". If your browser tab closes unexpectedly, your work remains safe.",[15,9845,9847],{"id":9846},"exporting-svg-and-png","Exporting SVG and PNG",[11,9849,510,9850,9852],{},[26,9851,9006],{}," section provides flexible saving options:",[298,9854,9855,9860,9865],{},[301,9856,9857,9859],{},[26,9858,9017],{}," — Automatically crops SVG\u002FPNG bounds tightly around visible vector paths.",[301,9861,9862,9864],{},[26,9863,8542],{}," — Preserves original canvas dimensions and padding margins.",[301,9866,9867,9870],{},[26,9868,9869],{},"Formats:",[298,9871,9872,9878,9884,9890,9898],{},[301,9873,9874,9877],{},[26,9875,9876],{},"SVG File"," — Downloads compact, production-ready SVG files. Adjacent paths combine and coordinate precision is rounded for minimal file size.",[301,9879,9880,9883],{},[26,9881,9882],{},"PNG File"," — Exports raster images with alpha channel transparency and custom DPI scale multipliers (e.g., 2x, 4x, 8x).",[301,9885,9886,9889],{},[26,9887,9888],{},"Copy SVG"," — Copies clean SVG code directly to your clipboard.",[301,9891,9892,9897],{},[26,9893,9894,9895,219],{},"Save current SVG (",[49,9896,9058],{}," — Directly overwrites the opened SVG file on disk (in browsers supporting File System Access API).",[301,9899,9900,9902],{},[26,9901,9064],{}," — Transfers generated vector code directly into the local SVG Optimizer tool for minification, attribute cleaning, and React\u002FVue\u002FData URI code export.",[15,9904,9905],{"id":9068},"Keyboard Shortcuts",[75,9907,9908,9916],{},[78,9909,9910],{},[81,9911,9912,9914],{},[84,9913,9078],{},[84,9915,9081],{},[91,9917,9918,9929,9940,9949,9960,9971,9983,9992,10002,10012,10023,10032,10044,10056,10065,10074,10083,10092,10101],{},[81,9919,9920,9926],{},[96,9921,9922,9541,9924],{},[49,9923,8732],{},[49,9925,9105],{},[96,9927,9928],{},"Select & Move shapes",[81,9930,9931,9937],{},[96,9932,9933,9541,9935],{},[49,9934,9552],{},[49,9936,1080],{},[96,9938,9939],{},"Edit nodes & Bézier curves",[81,9941,9942,9946],{},[96,9943,9944],{},[49,9945,9402],{},[96,9947,9948],{},"Pen tool",[81,9950,9951,9957],{},[96,9952,9953,9541,9955],{},[49,9954,8663],{},[49,9956,8675],{},[96,9958,9959],{},"Rectangle (Rect) \u002F Rounded Rectangle (Roundrect)",[81,9961,9962,9968],{},[96,9963,9964,9541,9966],{},[49,9965,8741],{},[49,9967,8693],{},[96,9969,9970],{},"Circle \u002F Ellipse",[81,9972,9973,9977],{},[96,9974,9975],{},[49,9976,5688],{},[96,9978,9979,9980,9982],{},"Straight Line (hold ",[49,9981,6136],{}," to snap to 45°)",[81,9984,9985,9989],{},[96,9986,9987],{},[49,9988,8711],{},[96,9990,9991],{},"Eyedropper tool",[81,9993,9994,9999],{},[96,9995,9996,9998],{},[49,9997,8715],{}," + drag node",[96,10000,10001],{},"Pull out symmetric Bézier handles from a corner node",[81,10003,10004,10009],{},[96,10005,10006,10008],{},[49,10007,8715],{}," + drag handle",[96,10010,10011],{},"Break handle symmetry (create a sharp corner node)",[81,10013,10014,10020],{},[96,10015,10016,9541,10018],{},[49,10017,9579],{},[49,10019,9582],{},[96,10021,10022],{},"Delete selected shape or node",[81,10024,10025,10029],{},[96,10026,10027],{},[49,10028,8863],{},[96,10030,10031],{},"Duplicate selected shape",[81,10033,10034,10041],{},[96,10035,10036,9541,10038],{},[49,10037,9668],{},[49,10039,10040],{},"[",[96,10042,10043],{},"Bring forward \u002F Send backward",[81,10045,10046,10053],{},[96,10047,10048,9541,10050],{},[49,10049,9190],{},[49,10051,10052],{},"Shift+Z",[96,10054,10055],{},"Undo and Redo",[81,10057,10058,10062],{},[96,10059,10060],{},[49,10061,8957],{},[96,10063,10064],{},"Toggle reference image",[81,10066,10067,10071],{},[96,10068,10069],{},[49,10070,8964],{},[96,10072,10073],{},"Toggle vector layer",[81,10075,10076,10080],{},[96,10077,10078],{},[49,10079,8970],{},[96,10081,10082],{},"Toggle side-by-side comparison mode",[81,10084,10085,10089],{},[96,10086,10087,9234],{},[49,10088,8980],{},[96,10090,10091],{},"Pan canvas",[81,10093,10094,10098],{},[96,10095,10096],{},[49,10097,9058],{},[96,10099,10100],{},"Overwrite opened SVG file",[81,10102,10103,10107],{},[96,10104,10105],{},[49,10106,6735],{},[96,10108,10109],{},"Open help & shortcuts modal",[15,10111,10113],{"id":10112},"step-by-step-workflow-from-png-raster-to-clean-svg","Step-by-Step Workflow: From PNG Raster to Clean SVG",[4001,10115,10116,10122,10129,10137,10143,10151,10156,10159],{},[301,10117,10118,10119,10121],{},"Load your raster icon via the ",[26,10120,8507],{}," button or drag-and-drop the file into your browser.",[301,10123,1838,10124,10126,10127,507],{},[26,10125,9448],{}," panel, set the target color count (e.g., 2 for monochrome logos) and enable ",[1321,10128,9466],{},[301,10130,8795,10131,10133,10134,10136],{},[26,10132,9479],{}," for solid silhouettes or ",[26,10135,9485],{}," for line art.",[301,10138,10139,10140,10142],{},"Click ",[26,10141,9448],{}," to inspect the generated vector paths.",[301,10144,10145,10146,10150],{},"Switch to the ",[26,10147,9549,10148,219],{},[49,10149,9552],{}," tool, align anchor nodes, and smooth curves using Bézier control handles.",[301,10152,1838,10153,10155],{},[26,10154,8755],{}," section, click your brand color to instantly recolor the icon.",[301,10157,10158],{},"Test icon contrast against dark and light preview backgrounds.",[301,10160,10161,10162,10164,10165,507],{},"Export via ",[26,10163,9017],{}," for a compact SVG or send the vector to ",[26,10166,10167],{},"SVG Optimizer",[11,10169,10170],{},"Vector Painter solves everyday vector tracing and icon editing tasks in seconds — without desktop software bloat, subscriptions, or uploading private graphics to third-party servers.",[15,10172,6614],{"id":357},[357,10174],{":slug":10175},"[\"vector-painter\",\"pixel-painter\",\"image-optimizer\",\"color-toolbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":10177},[10178,10179,10180,10181,10182,10183,10184,10185,10186,10187,10188,10189,10190],{"id":9367,"depth":364,"text":9368},{"id":9409,"depth":364,"text":9410},{"id":9441,"depth":364,"text":9442},{"id":9528,"depth":364,"text":9529},{"id":9654,"depth":364,"text":9655},{"id":9717,"depth":364,"text":9718},{"id":9743,"depth":364,"text":9744},{"id":9774,"depth":364,"text":9775},{"id":9827,"depth":364,"text":9828},{"id":9846,"depth":364,"text":9847},{"id":9068,"depth":364,"text":9905},{"id":10112,"depth":364,"text":10113},{"id":357,"depth":364,"text":6614},"2026-07-28","Vector Painter — trace PNG and WebP icons into editable SVG paths: refine Bézier nodes, apply brand colors, and export clean vector graphics locally.",[10194,10197,10200,10203,10206,10209,10212,10215],{"q":10195,"a":10196},"What is Vector Painter?","Vector Painter is a browser-based vector editor and automated image tracer. It allows you to convert raster images (PNG, WebP, JPEG) and pixel icons into clean SVG vector paths, edit anchor nodes and Bézier curves, build shapes from scratch, and export production-ready vector graphics — 100% locally in your browser.",{"q":10198,"a":10199},"Which file formats are supported?","The editor opens raster images (PNG, WebP, JPEG) and SVG files. You can also import raw SVG code, XML fragments, or path data strings (`d=\"M...\"`).",{"q":10201,"a":10202},"How does raster-to-vector tracing work?","The built-in engine analyzes reference pixels, separates background colors, quantizes 1 to 12 colors, and constructs vector geometry in three modes: Filled shapes, Centerlines (for line art), or Auto.",{"q":10204,"a":10205},"Can I manually edit Bézier curves, anchor nodes, and delete individual points?","Yes. The Edit nodes tool (`A`) lets you drag anchor points, pull out symmetric control handles with Alt-drag, or break handle symmetry for sharp corner nodes. Deleting nodes via Delete\u002FBackspace or double-click automatically reconnects adjacent points with recalculated smooth Bézier curve joins.",{"q":10207,"a":10208},"Does Vector Painter support vector shape primitives?","Yes. Beyond freehand Pen drawing (`P`) and auto-tracing, the editor includes Rectangles (`R`), Rounded Rectangles (`U`), Circles (`C`), Ellipses (`E`), and Straight Lines (`L`).",{"q":10210,"a":10211},"Can I use custom brand color palettes?","Yes. The editor uses the shared Brand Palette system. You can load custom brand colors, edit hex strings, persist them in local storage, and recolor vector shapes in a single click.",{"q":10213,"a":10214},"Are images uploaded to any server?","No. All importing, vector tracing, node editing, and exporting are processed 100% locally inside your browser. No data ever leaves your device.",{"q":10216,"a":10217},"What export options are available?","You can export compact SVG files, scaled PNG rasters, copy clean SVG code to your clipboard, overwrite opened SVG files directly (if supported by your browser), or pass vector code directly to the SVG Optimizer tool.","\u002Fimages\u002Fblog\u002Fblog_vector-painter.webp",{},"\u002Fblog\u002Fen\u002Fvector-painter",{"title":9356,"description":10192},"blog\u002Fen\u002Fvector-painter",[9349,9348,10224,10225,10226,8513],"vector editor","image tracing","Bézier curves","ilf8ggEOwQsa5zCs0tUhQVKe43D4xI61cD7PeLLR9hM",{"id":10229,"title":10230,"body":10231,"date":10579,"description":10580,"extension":376,"faq":10581,"image":10591,"lang":394,"meta":10592,"navigation":396,"path":10593,"published":396,"readTime":364,"seo":10594,"stem":10595,"tags":10596,"updated":405,"__hash__":10602},"blog\u002Fblog\u002Fen\u002Fhtaccess-builder.md","The Comprehensive .htaccess & mod_rewrite Guide for Apache",{"type":8,"value":10232,"toc":10567},[10233,10245,10249,10270,10274,10293,10352,10355,10375,10379,10386,10395,10399,10410,10435,10439,10456,10476,10480,10487,10555,10560,10562,10565],[11,10234,510,10235,10240,10241,10244],{},[26,10236,10237],{},[49,10238,10239],{},".htaccess"," (Hypertext Access) file is one of the most versatile tools available to developers and system administrators running the ",[26,10242,10243],{},"Apache HTTP Server",". It enables fine-grained control over URL redirects, security policies, browser caching, and routing rules on a per-directory basis.",[15,10246,10248],{"id":10247},"one-engineers-gift-to-apache-1996","One engineer's gift to Apache (1996)",[11,10250,10251,10256,10257,10260,10261,10266,10267,10269],{},[26,10252,10253],{},[49,10254,10255],{},"mod_rewrite"," — the module every one of these rules depends on — was written by a single developer, ",[26,10258,10259],{},"Ralf S. Engelschall",", in April 1996, who donated it to the Apache Software Foundation in July 1997. It wasn't his only contribution: two years later he also built ",[26,10262,10263],{},[49,10264,10265],{},"mod_ssl",", porting Ben Laurie's Apache-SSL patches so Apache could speak HTTPS at all. The two modules that make almost every ",[49,10268,10239],{}," file on the internet possible — rewriting and encryption — trace back to the same person.",[15,10271,10273],{"id":10272},"url-rewriting-engine-mod_rewrite","URL Rewriting Engine: mod_rewrite",[11,10275,10276,10277,10279,10280,10284,10285,10288,10289,10292],{},"At the core of ",[49,10278,10239],{}," lies ",[26,10281,10282],{},[49,10283,10255],{}," — a regular expression URL manipulation engine. It executes condition chains (",[49,10286,10287],{},"RewriteCond",") followed by action rules (",[49,10290,10291],{},"RewriteRule","):",[1564,10294,10298],{"className":10295,"code":10296,"language":10297,"meta":363,"style":363},"language-apache shiki shiki-themes github-dark","RewriteEngine On\n\n# Force HTTPS 301 Redirect\nRewriteCond %{HTTPS} off\nRewriteRule ^ https:\u002F\u002F%{HTTP_HOST}%{REQUEST_URI} [L,R=301]\n\n# Hide .php File Extensions\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteCond %{REQUEST_FILENAME}\\.php -f\nRewriteRule ^(.*)$ $1.php [L]\n","apache",[49,10299,10300,10305,10310,10315,10320,10325,10329,10334,10340,10346],{"__ignoreMap":363},[1572,10301,10302],{"class":1574,"line":1575},[1572,10303,10304],{},"RewriteEngine On\n",[1572,10306,10307],{"class":1574,"line":364},[1572,10308,10309],{"emptyLinePlaceholder":396},"\n",[1572,10311,10312],{"class":1574,"line":370},[1572,10313,10314],{},"# Force HTTPS 301 Redirect\n",[1572,10316,10317],{"class":1574,"line":1591},[1572,10318,10319],{},"RewriteCond %{HTTPS} off\n",[1572,10321,10322],{"class":1574,"line":1270},[1572,10323,10324],{},"RewriteRule ^ https:\u002F\u002F%{HTTP_HOST}%{REQUEST_URI} [L,R=301]\n",[1572,10326,10327],{"class":1574,"line":4389},[1572,10328,10309],{"emptyLinePlaceholder":396},[1572,10330,10331],{"class":1574,"line":742},[1572,10332,10333],{},"# Hide .php File Extensions\n",[1572,10335,10337],{"class":1574,"line":10336},8,[1572,10338,10339],{},"RewriteCond %{REQUEST_FILENAME} !-d\n",[1572,10341,10343],{"class":1574,"line":10342},9,[1572,10344,10345],{},"RewriteCond %{REQUEST_FILENAME}\\.php -f\n",[1572,10347,10349],{"class":1574,"line":10348},10,[1572,10350,10351],{},"RewriteRule ^(.*)$ $1.php [L]\n",[11,10353,10354],{},"Flags in square brackets control execution flow:",[298,10356,10357,10363,10369],{},[301,10358,10359,10362],{},[49,10360,10361],{},"[L]"," (Last): Stop evaluating subsequent rules if current rule matches.",[301,10364,10365,10368],{},[49,10366,10367],{},"[R=301]",": Emit an HTTP 301 Moved Permanently response to the client.",[301,10370,10371,10374],{},[49,10372,10373],{},"[NC]"," (No Case): Perform case-insensitive regex matching.",[15,10376,10378],{"id":10377},"security-performance-core","Security & Performance Core",[70,10380,10382,10383,219],{"id":10381},"_1-disable-directory-listing-options-indexes","1. Disable Directory Listing (",[49,10384,10385],{},"Options -Indexes",[11,10387,10388,10389,10391,10392,10394],{},"If an index file is absent from a folder, ",[49,10390,10385],{}," prevents Apache from rendering raw directory contents, serving a ",[49,10393,3463],{}," response instead.",[70,10396,10398],{"id":10397},"_2-protect-sensitive-files","2. Protect Sensitive Files",[11,10400,10401,10402,673,10404,673,10407,10409],{},"Environment configuration files (",[49,10403,3786],{},[49,10405,10406],{},".git",[49,10408,10239],{},") often hold database passwords and API tokens:",[1564,10411,10413],{"className":10295,"code":10412,"language":10297,"meta":363,"style":363},"\u003CFilesMatch \"^\\.|\\.env$|\\.git\">\n    Order allow,deny\n    Deny from all\n\u003C\u002FFilesMatch>\n",[49,10414,10415,10420,10425,10430],{"__ignoreMap":363},[1572,10416,10417],{"class":1574,"line":1575},[1572,10418,10419],{},"\u003CFilesMatch \"^\\.|\\.env$|\\.git\">\n",[1572,10421,10422],{"class":1574,"line":364},[1572,10423,10424],{},"    Order allow,deny\n",[1572,10426,10427],{"class":1574,"line":370},[1572,10428,10429],{},"    Deny from all\n",[1572,10431,10432],{"class":1574,"line":1591},[1572,10433,10434],{},"\u003C\u002FFilesMatch>\n",[70,10436,10438],{"id":10437},"_3-compression-and-browser-caching","3. Compression and Browser Caching",[11,10440,10441,10442,1121,10445,10448,10449,9541,10452,10455],{},"Modules ",[49,10443,10444],{},"mod_deflate",[49,10446,10447],{},"mod_expires"," compress text resources with Gzip and set standard ",[49,10450,10451],{},"Cache-Control",[49,10453,10454],{},"Expires"," headers:",[1564,10457,10459],{"className":10295,"code":10458,"language":10297,"meta":363,"style":363},"\u003CIfModule mod_deflate.c>\n    AddOutputFilterByType DEFLATE text\u002Fhtml text\u002Fcss application\u002Fjavascript application\u002Fjson\n\u003C\u002FIfModule>\n",[49,10460,10461,10466,10471],{"__ignoreMap":363},[1572,10462,10463],{"class":1574,"line":1575},[1572,10464,10465],{},"\u003CIfModule mod_deflate.c>\n",[1572,10467,10468],{"class":1574,"line":364},[1572,10469,10470],{},"    AddOutputFilterByType DEFLATE text\u002Fhtml text\u002Fcss application\u002Fjavascript application\u002Fjson\n",[1572,10472,10473],{"class":1574,"line":370},[1572,10474,10475],{},"\u003C\u002FIfModule>\n",[15,10477,10479],{"id":10478},"features-of-our-htaccess-builder","Features of Our .htaccess Builder",[11,10481,8355,10482,10486],{},[38,10483,10485],{"href":10484},"\u002Ftools\u002Fhtaccess-builder",".htaccess Generator & Rewrite Rule Constructor"," produces clean, battle-tested Apache configurations:",[298,10488,10489,10502,10514,10527,10546],{},[301,10490,10491,10494,10495,8395,10498,10501],{},[26,10492,10493],{},"HTTPS & Canonical Domain Rules",": One-click toggles for HTTPS 301 redirects and ",[49,10496,10497],{},"www",[49,10499,10500],{},"non-www"," canonical URL normalization.",[301,10503,10504,10507,10508,1121,10511,10513],{},[26,10505,10506],{},"Clean Extensionless URLs",": Hide ",[49,10509,10510],{},".php",[49,10512,7070],{}," extensions automatically.",[301,10515,10516,10519,10520,10523,10524,507],{},[26,10517,10518],{},"Custom 301 Redirect Table",": Interactive table for mapping legacy paths ",[49,10521,10522],{},"\u002Fold-url"," ➔ ",[49,10525,10526],{},"\u002Fnew-url",[301,10528,10529,10532,10533,673,10535,8395,10537,10539,10540,10542,10543,10545],{},[26,10530,10531],{},"Security & Caching Directives",": Toggles for ",[49,10534,10385],{},[49,10536,3786],{},[49,10538,10406],{}," blocking, ",[49,10541,10444],{}," Gzip, and ",[49,10544,10447],{}," headers.",[301,10547,10548,10551,10552,10554],{},[26,10549,10550],{},"Direct File Download",": Export formatted ",[49,10553,10239],{}," files straight to your machine.",[11,10556,10557,10559],{},[26,10558,353],{}," open the tool → pick a stack preset (WordPress, Joomla, Laravel, or General) → review the Security & Firewall tab → add any custom redirects you need → copy or download the file → upload it to your site's document root.",[15,10561,6614],{"id":357},[357,10563],{":slug":10564},"[\"htaccess-builder\",\"apache-builder\",\"nginx-builder\",\"csp-builder\"]",[1779,10566,2290],{},{"title":363,"searchDepth":364,"depth":364,"links":10568},[10569,10570,10571,10577,10578],{"id":10247,"depth":364,"text":10248},{"id":10272,"depth":364,"text":10273},{"id":10377,"depth":364,"text":10378,"children":10572},[10573,10575,10576],{"id":10381,"depth":370,"text":10574},"1. Disable Directory Listing (Options -Indexes)",{"id":10397,"depth":370,"text":10398},{"id":10437,"depth":370,"text":10438},{"id":10478,"depth":364,"text":10479},{"id":357,"depth":364,"text":6614},"2026-07-27","How to configure Apache .htaccess: forced HTTPS 301 redirects, canonical domains, hiding .php and .html file extensions, Gzip compression, and security rules.",[10582,10585,10588],{"q":10583,"a":10584},"What is an .htaccess file and how does Apache use it?",".htaccess (Hypertext Access) is a directory-level configuration file for the Apache HTTP Server. It allows developers to customize server behavior (URL rewrite rules, headers, access control) per directory without modifying the global httpd.conf file or restarting Apache.",{"q":10586,"a":10587},"What is the difference between a 301 and a 302 redirect?","A 301 (Moved Permanently) redirect informs search engines and browsers that a URL has permanently moved to a new destination, transferring 90-99% of SEO link equity. A 302 (Found \u002F Temporary) redirect indicates a temporary relocation.",{"q":10589,"a":10590},"Why is Options -Indexes recommended?","By default, if no index file (index.html, index.php) exists in an Apache directory, the server displays a public file list. Options -Indexes disables directory browsing and returns a 403 Forbidden status, protecting source files and database backups.","\u002Fimages\u002Fblog\u002Fblog_htaccess-builder.webp",{},"\u002Fblog\u002Fen\u002Fhtaccess-builder",{"title":10230,"description":10580},"blog\u002Fen\u002Fhtaccess-builder",[10597,10598,10599,10385,10600,10601],".htaccess generator","Apache mod_rewrite","HTTPS 301 redirect","mod_deflate gzip","mod_expires caching","6tNZqmSz9XawtGReZ64p8elri3OtoXF9A95SPPla3r8",{"id":10604,"title":10605,"body":10606,"date":10949,"description":10950,"extension":376,"faq":10951,"image":10961,"lang":394,"meta":10962,"navigation":396,"path":10963,"published":396,"readTime":370,"seo":10964,"stem":10965,"tags":10966,"updated":405,"__hash__":10973},"blog\u002Fblog\u002Fen\u002Fcsp-builder.md","Content Security Policy (CSP 3): XSS Defense Guide",{"type":8,"value":10607,"toc":10941},[10608,10615,10621,10625,10640,10651,10655,10661,10672,10679,10685,10689,10809,10813,10850,10854,10861,10923,10934,10936,10939],[11,10609,10610,10611,10614],{},"Cross-Site Scripting (XSS) consistently ranks among the most critical vulnerabilities in the ",[26,10612,10613],{},"OWASP Top 10",". No matter how secure a backend API is, a single unescaped user input can allow an attacker to inject malicious JavaScript and hijack user sessions.",[11,10616,10617,10618,507],{},"The browser's primary defense line against XSS is ",[26,10619,10620],{},"Content Security Policy (CSP)",[15,10622,10624],{"id":10623},"born-at-mozilla-20042011","Born at Mozilla (2004–2011)",[11,10626,10627,10628,10631,10632,10635,10636,10639],{},"The idea of a page declaring its own content restrictions goes back to at least 2007, floated independently by Mozilla's Gervase Markham and security researcher Robert \"RSnake\" Hansen. ",[26,10629,10630],{},"Brandon Sterne",", then Mozilla's security program manager, turned the idea into working code with engineer Sid Stamm, and their pre-spec implementation shipped quietly in ",[26,10633,10634],{},"Firefox 4"," in March 2011 under a different, now-obsolete header: ",[49,10637,10638],{},"X-Content-Security-Policy",". Sterne even built a bookmarklet that scanned a live page and suggested a starting policy for it.",[11,10641,10642,10643,10646,10647,10650],{},"Chrome followed a few months later with its own prefixed header, ",[49,10644,10645],{},"X-Webkit-CSP",". The two implementations didn't agree on syntax, so when the W3C published its first Content Security Policy 1.0 working draft in November 2011, the standardized version differed from what either browser had already shipped — Firefox didn't catch up to the plain ",[49,10648,10649],{},"Content-Security-Policy"," header and CSP 1.0 directive syntax until Firefox 23, in 2013.",[15,10652,10654],{"id":10653},"how-browsers-enforce-csp","How Browsers Enforce CSP",[11,10656,10657,10658,10660],{},"When a browser receives the ",[49,10659,10649],{}," header, it enforces a strict allowlist. For every resource (JS file, stylesheet, image, WebSocket connection), the browser verifies the request origin against declared rules:",[1564,10662,10666],{"className":10663,"code":10664,"language":10665,"meta":363,"style":363},"language-http shiki shiki-themes github-dark","Content-Security-Policy: default-src 'self'; script-src 'self' https:\u002F\u002Fcdn.jsdelivr.net; object-src 'none'; frame-ancestors 'none'; upgrade-insecure-requests;\n","http",[49,10667,10668],{"__ignoreMap":363},[1572,10669,10670],{"class":1574,"line":1575},[1572,10671,10664],{},[11,10673,10674,10675,10678],{},"If a script attempts to load from an unauthorized domain or execute an inline block when ",[49,10676,10677],{},"unsafe-inline"," is disallowed, the browser blocks execution immediately and logs an error to the developer console:",[1564,10680,10683],{"className":10681,"code":10682,"language":4815,"meta":363},[4813],"Refused to execute inline script because it violates the following Content Security Policy directive: \"script-src 'self'\".\n",[49,10684,10682],{"__ignoreMap":363},[15,10686,10688],{"id":10687},"essential-csp-3-directives","Essential CSP 3 Directives",[298,10690,10691,10699,10716,10724,10740,10759,10770,10785,10797],{},[301,10692,10693,10698],{},[26,10694,10695],{},[49,10696,10697],{},"default-src",": Fallback policy for resource types without explicit directives.",[301,10700,10701,10706,10707,673,10710,673,10713,8399],{},[26,10702,10703],{},[49,10704,10705],{},"script-src",": Controls valid sources of JavaScript (",[49,10708,10709],{},"'self'",[49,10711,10712],{},"'nonce-...'",[49,10714,10715],{},"'sha256-...'",[301,10717,10718,10723],{},[26,10719,10720],{},[49,10721,10722],{},"style-src",": Controls valid sources of CSS stylesheets.",[301,10725,10726,10731,10732,673,10734,673,10737,8399],{},[26,10727,10728],{},[49,10729,10730],{},"img-src",": Authorized image sources (",[49,10733,10709],{},[49,10735,10736],{},"data:",[49,10738,10739],{},"https:",[301,10741,10742,10747,10748,673,10751,673,10754,1228,10756,507],{},[26,10743,10744],{},[49,10745,10746],{},"connect-src",": Restricts target URLs for ",[49,10749,10750],{},"fetch()",[49,10752,10753],{},"XMLHttpRequest",[49,10755,2499],{},[49,10757,10758],{},"EventSource",[301,10760,10761,10766,10767,507],{},[26,10762,10763],{},[49,10764,10765],{},"object-src",": Restricts browser plugins (Flash, Java). Should always be set to ",[49,10768,10769],{},"'none'",[301,10771,10772,10777,10778,10781,10782,507],{},[26,10773,10774],{},[49,10775,10776],{},"frame-ancestors",": Replaces legacy ",[49,10779,10780],{},"X-Frame-Options"," to prevent Clickjacking attacks by controlling which sites can embed the page in an ",[49,10783,10784],{},"\u003Ciframe>",[301,10786,10787,10792,10793,10796],{},[26,10788,10789],{},[49,10790,10791],{},"base-uri",": OWASP-recommended directive that restricts what a ",[49,10794,10795],{},"\u003Cbase>"," tag is allowed to point to, closing off an easy way to hijack every relative URL on the page.",[301,10798,10799,10804,10805,10808],{},[26,10800,10801],{},[49,10802,10803],{},"form-action",": OWASP-recommended directive limiting where ",[49,10806,10807],{},"\u003Cform>"," submissions can go, so an injected form can't quietly ship a password to another domain.",[15,10810,10812],{"id":10811},"critical-csp-configuration-pitfalls","Critical CSP Configuration Pitfalls",[4001,10814,10815,10827,10840],{},[301,10816,10817,10826],{},[26,10818,10819,10820,10823,10824],{},"Including ",[49,10821,10822],{},"'unsafe-inline'"," in ",[49,10825,10705],{},": Leaves the application open to inline XSS execution.",[301,10828,10829,10834,10835,1121,10837,507],{},[26,10830,10819,10831],{},[49,10832,10833],{},"'unsafe-eval'",": Permits dynamic string evaluation via ",[49,10836,1709],{},[49,10838,10839],{},"Function()",[301,10841,10842,10849],{},[26,10843,10844,10845,10848],{},"Wildcard ",[49,10846,10847],{},"*"," rules",": Allowing all origins undermines the security boundaries of CSP.",[15,10851,10853],{"id":10852},"features-of-our-csp-builder","Features of Our CSP Builder",[11,10855,8355,10856,10860],{},[38,10857,10859],{"href":10858},"\u002Ftools\u002Fcsp-builder","Content Security Policy & Header Builder"," simplifies crafting secure header policies:",[298,10862,10863,10880,10886,10905],{},[301,10864,10865,10868,10869,673,10871,673,10873,673,10875,1228,10877,10879],{},[26,10866,10867],{},"Quick Directive Chips",": Toggle ",[49,10870,10709],{},[49,10872,10822],{},[49,10874,10739],{},[49,10876,10736],{},[49,10878,10769],{}," with a single click.",[301,10881,10882,10885],{},[26,10883,10884],{},"Automated OWASP Audit",": Real-time safety score (A+, B, F) with warning flags for risky directives.",[301,10887,10888,10891,10892,4110,10895,10898,10899,1228,10902,507],{},[26,10889,10890],{},"Security Suite Integration",": Add ",[26,10893,10894],{},"HSTS",[49,10896,10897],{},"Strict-Transport-Security","), ",[49,10900,10901],{},"X-Frame-Options: DENY",[49,10903,10904],{},"X-Content-Type-Options: nosniff",[301,10906,10907,10910,10911,10914,10915,10918,10919,10922],{},[26,10908,10909],{},"Multi-Format Exporter",": Generate directives for Nginx (",[49,10912,10913],{},"add_header","), Apache (",[49,10916,10917],{},"Header set","), HTML ",[49,10920,10921],{},"\u003Cmeta>",", and HTTP response headers.",[11,10924,10925,10927,10928,10930,10931,10933],{},[26,10926,353],{}," open the tool → keep the ",[49,10929,10709],{}," defaults → add any CDN or font origins you actually use with the chips → check the score badge → switch export format to match your server (Nginx, Apache, or a ",[49,10932,10921],{}," tag) → copy and deploy.",[15,10935,6614],{"id":357},[357,10937],{":slug":10938},"[\"csp-builder\",\"htaccess-builder\",\"nginx-builder\",\"apache-builder\"]",[1779,10940,2290],{},{"title":363,"searchDepth":364,"depth":364,"links":10942},[10943,10944,10945,10946,10947,10948],{"id":10623,"depth":364,"text":10624},{"id":10653,"depth":364,"text":10654},{"id":10687,"depth":364,"text":10688},{"id":10811,"depth":364,"text":10812},{"id":10852,"depth":364,"text":10853},{"id":357,"depth":364,"text":6614},"2026-07-25","How to properly configure Content-Security-Policy headers, eliminate unsafe-inline risks, combine HSTS & X-Frame-Options, and export Nginx & Apache rules.",[10952,10955,10958],{"q":10953,"a":10954},"What is Content Security Policy (CSP)?","Content Security Policy is an HTTP response header (or HTML meta tag) that instructs the browser which origins are allowed to load and execute resources (scripts, styles, images, frames, fonts).",{"q":10956,"a":10957},"Why is 'unsafe-inline' dangerous in script-src?","'unsafe-inline' allows the browser to execute inline \u003Cscript> tags and inline event handlers (like onclick). If your site has a Cross-Site Scripting (XSS) vulnerability, 'unsafe-inline' neutralizes CSP protection completely.",{"q":10959,"a":10960},"What is the difference between default-src and script-src?","default-src acts as a fallback for directive types that are not explicitly defined. If script-src or style-src is omitted, the browser falls back to the rules in default-src.","\u002Fimages\u002Fblog\u002Fblog_csp-builder.webp",{},"\u002Fblog\u002Fen\u002Fcsp-builder",{"title":10605,"description":10950},"blog\u002Fen\u002Fcsp-builder",[10967,10968,10969,10970,10971,10972],"Content Security Policy","CSP 3 headers","XSS defense","unsafe-inline risk","Nginx add_header CSP","Apache Header set","4Qy0matzbLYeJEPoZYSOnzkJSG2E9ULv1vFVp4W5-uM",{"id":10975,"title":10976,"body":10977,"date":11214,"description":11215,"extension":376,"faq":11216,"image":11226,"lang":394,"meta":11227,"navigation":396,"path":11228,"published":396,"readTime":370,"seo":11229,"stem":11230,"tags":11231,"updated":405,"__hash__":11237},"blog\u002Fblog\u002Fen\u002Foauth-pkce-toolbox.md","Securing OAuth 2.0 Applications with PKCE (RFC 7636)",{"type":8,"value":10978,"toc":11207},[10979,10990,10994,11005,11009,11015,11022,11026,11050,11056,11062,11142,11146,11153,11191,11202,11204],[11,10980,10981,10982,10985,10986,10989],{},"In the early days of ",[26,10983,10984],{},"OAuth 2.0 (RFC 6749)",", authorization code flows depended heavily on static ",[49,10987,10988],{},"client_secret"," strings stored on backend servers. However, as modern web architecture shifted towards Single Page Applications (React, Vue, Nuxt) and mobile clients, exposing secrets in client-side code created severe security risks.",[15,10991,10993],{"id":10992},"oauth-20s-messy-birth-2012","OAuth 2.0's messy birth (2012)",[11,10995,10996,10997,11000,11001,11004],{},"OAuth 2.0 didn't arrive quietly. ",[26,10998,10999],{},"Eran Hammer",", its lead author and editor, resigned from the working group in July 2012 — months before RFC 6749 was even published — and posted a blog titled ",[1321,11002,11003],{},"\"OAuth 2.0 and the Road to Hell.\""," His complaint: the spec had grown \"more complex, less interoperable, less useful, more incomplete, and most importantly, less secure\" than OAuth 1.0, having dropped mandatory signatures in favor of bearer tokens and left too many details for each vendor to improvise. RFC 6749 shipped that October anyway, without his name on it. PKCE exists in large part to patch exactly the kind of gap that criticism predicted.",[15,11006,11008],{"id":11007},"the-public-client-vulnerability","The Public Client Vulnerability",[11,11010,11011,11012,11014],{},"Public clients (browser SPAs, native iOS\u002FAndroid apps) cannot keep secrets safe. End users can inspect JavaScript code or decompile mobile binaries to extract hardcoded ",[49,11013,10988],{}," keys.",[11,11016,11017,11018,11021],{},"Furthermore, malicious software (browser extensions or custom URI handlers on mobile devices) could intercept authorization codes returned to ",[49,11019,11020],{},"redirect_uri"," endpoints and exchange them for user access tokens.",[15,11023,11025],{"id":11024},"how-pkce-rfc-7636-resolves-the-threat","How PKCE (RFC 7636) Resolves the Threat",[11,11027,11028,11029,673,11032,1228,11035,11038,11039,11042,11043,11046,11047,11049],{},"Published in September 2015 by ",[26,11030,11031],{},"Nat Sakimura",[26,11033,11034],{},"John Bradley",[26,11036,11037],{},"Naveen Agarwal",", RFC 7636 wasn't originally written with SPAs in mind — its motivating scenario was ",[26,11040,11041],{},"native mobile apps",". Two apps could register for the same custom URI scheme (",[49,11044,11045],{},"myapp:\u002F\u002Fcallback","), letting a malicious app intercept the authorization code meant for the legitimate one. PKCE (pronounced \"pixy\") closed that hole, and the same technique turned out to fit browser-based public clients just as well once the industry realized a ",[49,11048,10988],{}," embedded in JavaScript is no safer than one embedded in a mobile binary.",[11,11051,11052,11055],{},[26,11053,11054],{},"Proof Key for Code Exchange"," eliminates the need for static client secrets in public apps:",[1564,11057,11060],{"className":11058,"code":11059,"language":4815,"meta":363},[4813],"  Client Browser (SPA)                    Auth Server (IDP)\n        |                                       |\n        |---- (A) Authorization Request ------->|\n        |     + code_challenge (S256)           |\n        |                                       |\n        |\u003C--- (B) Authorization Code -----------|\n        |                                       |\n        |---- (C) Token Request --------------->|\n        |     + code_verifier                   |\n        |                                       |\n        |\u003C--- (D) Access Token -----------------|\n        |                                       |\n",[49,11061,11059],{"__ignoreMap":363},[4001,11063,11064,11091,11111],{},[301,11065,11066,8342,11069],{},[26,11067,11068],{},"Client-side Cryptographic Pair",[298,11070,11071,11080],{},[301,11072,11073,11074,11079],{},"The browser generates a random ",[26,11075,11076],{},[49,11077,11078],{},"code_verifier"," (43-128 chars).",[301,11081,11082,11083,1648,11088,507],{},"It derives a ",[26,11084,11085],{},[49,11086,11087],{},"code_challenge",[49,11089,11090],{},"Base64URL(SHA256(code_verifier))",[301,11092,11093,8342,11096],{},[26,11094,11095],{},"Authorization Request",[298,11097,11098],{},[301,11099,11100,11101,673,11104,673,11106,1228,11108,507],{},"The client redirects the user to the IDP passing ",[49,11102,11103],{},"client_id",[49,11105,11020],{},[49,11107,11087],{},[49,11109,11110],{},"code_challenge_method=S256",[301,11112,11113,8342,11116],{},[26,11114,11115],{},"Code Exchange",[298,11117,11118,11121,11132],{},[301,11119,11120],{},"After user authentication, the authorization code is issued.",[301,11122,11123,11124,11127,11128,507],{},"The client sends a ",[49,11125,11126],{},"POST \u002Ftoken"," request with the unhashed ",[26,11129,11130],{},[49,11131,11078],{},[301,11133,11134,11135,11138,11139,507],{},"The server computes ",[49,11136,11137],{},"SHA256(code_verifier)"," and verifies it against the initial challenge before releasing the ",[49,11140,11141],{},"access_token",[15,11143,11145],{"id":11144},"features-of-our-oauth-pkce-sandbox","Features of Our OAuth PKCE Sandbox",[11,11147,8355,11148,11152],{},[38,11149,11151],{"href":11150},"\u002Ftools\u002Foauth-pkce-toolbox","OAuth 2.0 PKCE Generator & Auth Flow Sandbox"," empowers developers to debug authentication flows:",[298,11154,11155,11164,11170,11179],{},[301,11156,11157,11160,11161,8399],{},[26,11158,11159],{},"S256 PKCE Pair Generator",": Instant computation using the browser's native Web Crypto API (",[49,11162,11163],{},"window.crypto.subtle",[301,11165,11166,11169],{},[26,11167,11168],{},"Auth URL Builder",": Provider presets for Google OAuth 2.0, GitHub, Microsoft Entra ID, and Auth0.",[301,11171,11172,11175,11176,11178],{},[26,11173,11174],{},"cURL Exchange Command Generator",": Generate formatted terminal requests to test ",[49,11177,11126],{}," endpoints.",[301,11180,11181,11184,11185,673,11187,11190],{},[26,11182,11183],{},"Callback Response Parser",": Extract ",[49,11186,49],{},[49,11188,11189],{},"state",", and error details from login response query strings.",[11,11192,11193,11195,11196,11198,11199,11201],{},[26,11194,353],{}," open the tool → generate a fresh PKCE pair → switch to Auth URL Builder → pick a provider preset → copy the URL and open it in a tab → after login, paste the callback URL into the Callback Parser → use the extracted ",[49,11197,49],{}," plus your ",[49,11200,11078],{}," in the Token Exchange cURL tab.",[15,11203,6614],{"id":357},[357,11205],{":slug":11206},"[\"oauth-pkce-toolbox\",\"jwt-inspector\",\"hash-generator\",\"encryption-toolbox\",\"webhook-verifier\"]",{"title":363,"searchDepth":364,"depth":364,"links":11208},[11209,11210,11211,11212,11213],{"id":10992,"depth":364,"text":10993},{"id":11007,"depth":364,"text":11008},{"id":11024,"depth":364,"text":11025},{"id":11144,"depth":364,"text":11145},{"id":357,"depth":364,"text":6614},"2026-07-23","Why Client Secrets fail in Public Clients, how code_verifier and S256 challenges work, and how to test OAuth 2.0 authorization flows.",[11217,11220,11223],{"q":11218,"a":11219},"Why is PKCE (Proof Key for Code Exchange) required for SPAs?","Single Page Applications (SPAs) and mobile apps are public clients that cannot securely store a Client Secret. PKCE prevents Authorization Code Interception Attacks by dynamically creating a one-time cryptographic verifier and challenge pair for every login attempt.",{"q":11221,"a":11222},"What is the difference between plain and S256 code_challenge_method?","The plain method sends the raw code_verifier in cleartext as the challenge, which is vulnerable to network sniffing. The S256 method hashes the code_verifier with SHA-256 and encodes it with Base64URL — making it the required standard for RFC 7636.",{"q":11224,"a":11225},"What is the valid length of a code_verifier?","Per RFC 7636, a valid code_verifier string must be between 43 and 128 characters long, containing unreserved URL characters [A-Z, a-z, 0-9, -, ., _, ~].","\u002Fimages\u002Fblog\u002Fblog_oauth-pkce-toolbox.webp",{},"\u002Fblog\u002Fen\u002Foauth-pkce-toolbox",{"title":10976,"description":11215},"blog\u002Fen\u002Foauth-pkce-toolbox",[11232,11233,11078,11234,11235,11236],"OAuth 2.0 PKCE","RFC 7636","code_challenge S256","OAuth authorization code","web security","AADJGGtxAOepnRa6SwGxFQHYWPslEttZD1imYEj4PB0",{"id":11239,"title":11240,"body":11241,"date":11447,"description":11448,"extension":376,"faq":11449,"image":11462,"lang":394,"meta":11463,"navigation":396,"path":11464,"published":396,"readTime":1270,"seo":11465,"stem":11466,"tags":11467,"updated":405,"__hash__":11474},"blog\u002Fblog\u002Fen\u002Fqr-code-history.md","Why Denso Wave Made QR Codes Free to Use",{"type":8,"value":11242,"toc":11438},[11243,11246,11249,11253,11262,11265,11272,11281,11284,11288,11291,11300,11309,11313,11316,11319,11330,11334,11337,11381,11384,11387,11390,11396,11400,11403,11406,11409,11413,11420,11427,11430,11433,11435],[11,11244,11245],{},"QR codes have become an ordinary bridge between a physical object and a digital action. We point a camera at a menu, ticket or package and rarely notice how many engineering decisions sit inside the small grid. Its story did not begin with smartphones or payments. It began with a factory problem: workers needed to read more information, more quickly, than a conventional barcode could hold.",[11,11247,11248],{},"The decision made after development proved just as important. Denso did not abandon all of its patents, but it published the specification and allowed standardized QR Code to be used freely. That distinction sounds legalistic, yet it explains how a corporate technology became a shared international format.",[15,11250,11252],{"id":11251},"manufacturing-had-outgrown-one-barcode","Manufacturing had outgrown one barcode",[11,11254,11255,11256,11261],{},"In the early 1990s, automotive production was moving from long runs of identical products toward flexible manufacturing of many variants. More information had to travel with parts and finished goods. According to ",[38,11257,11260],{"href":11258,"rel":11259},"https:\u002F\u002Fwww.denso.com\u002Fglobal\u002Fen\u002Fdriven-base\u002Fcareer-life\u002Fqr-hara_2408\u002F",[429],"Denso's history",", a linear barcode held about twenty alphanumeric characters, so workers sometimes had to scan roughly ten codes on one product. Oil contamination could make those labels harder to read as well.",[11,11263,11264],{},"Masahiro Hara and one other engineer began working on a replacement in 1992. It is worth keeping the corporate timeline straight: they were part of Denso Corporation at the time, and Denso Wave became a separate company after the business was spun off in 2001. The team wanted high capacity, support for kanji and kana, a compact footprint and fast recognition.",[11,11266,11267,11268,11271],{},"The hardest problem was not simply putting data in two dimensions. Other 2D codes already existed, but a scanner still had to find the symbol quickly among text, borders and other printed marks. Hara proposed adding dedicated position markers. The team then surveyed patterns in newspapers, magazines, leaflets and packaging to find a black-and-white sequence that appeared rarely. The result was the ",[49,11269,11270],{},"1:1:3:1:1"," ratio found across each large corner square. A scanner can search for that ratio to locate the code and determine its rotation.",[11,11273,11274,11275,11280],{},"The game of Go is often turned into a single eureka story, as though the sight of black and white stones immediately supplied the finished matrix. Hara has indeed described Go as one source of his thinking: the position of stones remains recognizable from context even when a pattern is imperfect. The detailed ",[38,11276,11279],{"href":11277,"rel":11278},"https:\u002F\u002Fwww.qrcode.com\u002Fen\u002Fhistory\u002F",[429],"Denso Wave development history",", however, traces the finder pattern to the separate problem of rapid location and the measurement of rare black-and-white proportions. Both parts belong in the story, but they should not be compressed into a tidy myth.",[11,11282,11283],{},"After a year and a half of development, the design was ready. Denso announced Quick Response Code in 1994, and its name stated the main priority plainly: fast reading.",[15,11285,11287],{"id":11286},"the-patents-stayed-but-standardized-qr-code-was-opened","The patents stayed, but standardized QR Code was opened",[11,11289,11290],{},"The familiar line that Denso “gave QR Code to the world” captures the broad effect while skipping the mechanism. The company retained patent rights and publicly promised not to exercise them against standards-compliant QR codes. Other companies could obtain the specification and build compatible readers, printers and software without negotiating a separate Denso Wave licence.",[11,11292,11293,11294,11299],{},"Standardization happened in stages. QR Code became an AIM International standard in 1997, entered JIS in 1999 and was accepted as ISO\u002FIEC 18004 in June 2000. The current fourth edition at the time of writing, ",[38,11295,11298],{"href":11296,"rel":11297},"https:\u002F\u002Fwww.iso.org\u002Fstandard\u002F83389.html",[429],"ISO\u002FIEC 18004:2024",", covers symbol structure, data-encoding methods, dimensions, error-correction rules, a reference decoding algorithm and production-quality requirements.",[11,11301,11302,11303,11308],{},"The promise also has a boundary. On its ",[38,11304,11307],{"href":11305,"rel":11306},"https:\u002F\u002Fwww.qrcode.com\u002Fen\u002Fabout\u002Fstandards.html",[429],"standardization page",", Denso Wave identifies patent JP2938338 and speaks specifically about standardized QR codes. A non-standard design may involve other rights, and the words QR Code are a registered trademark. Ordinary creation and commercial use of a symbol conforming to JIS or ISO requires no application, licence contract or fee paid to Denso Wave.",[15,11310,11312],{"id":11311},"a-qr-symbol-contains-functional-geometry-and-a-data-stream","A QR symbol contains functional geometry and a data stream",[11,11314,11315],{},"The black and white squares are called modules, but not every module carries a piece of the link or text. A light quiet zone at least four modules wide surrounds a normal QR symbol. Three finder patterns locate and orient it, timing patterns establish the rhythm of rows and columns, and alignment patterns from Version 2 onward help compensate for geometric distortion. Nearby format information records settings including the error-correction level and mask.",[11,11317,11318],{},"The remaining region contains encoded data and Reed-Solomon check codewords. They should not be pictured as two neat halves of the matrix. Data is divided into blocks, extended with check codewords and placed through the grid in a defined order. A mask then flips selected light and dark modules to avoid patterns that are difficult to scan.",[11,11320,11321,11322,11325,11326,11329],{},"Standard QR Code has 40 versions. Version 1 measures ",[49,11323,11324],{},"21 × 21"," modules, and each later version adds four modules to each side, ending at ",[49,11327,11328],{},"177 × 177"," for Version 40. This is why an unqualified statement that “a QR code holds this many characters” is incomplete. Capacity depends on the version, numeric, alphanumeric, byte or kanji mode, and the selected error-correction level.",[15,11331,11333],{"id":11332},"error-correction-percentages-do-not-describe-image-area","Error-correction percentages do not describe image area",[11,11335,11336],{},"QR Code offers four restoration levels. Denso Wave summarizes them as follows:",[75,11338,11339,11350],{},[78,11340,11341],{},[81,11342,11343,11346],{},[84,11344,11345],{},"Level",[84,11347,11349],{"align":11348},"right","Approximate share of codewords that can be restored",[91,11351,11352,11359,11366,11374],{},[81,11353,11354,11356],{},[96,11355,5688],{},[96,11357,11358],{"align":11348},"7%",[81,11360,11361,11363],{},[96,11362,8527],{},[96,11364,11365],{"align":11348},"15%",[81,11367,11368,11371],{},[96,11369,11370],{},"Q",[96,11372,11373],{"align":11348},"25%",[81,11375,11376,11378],{},[96,11377,8957],{},[96,11379,11380],{"align":11348},"30%",[11,11382,11383],{},"The important term is “codewords.” Level H does not guarantee that any 30% of the image may be painted over. A smaller defect in the wrong place may damage a finder pattern, the quiet zone or too many codewords from one block. Many scattered printing defects and one solid logo create different failure modes even if they occupy a similar area.",[11,11385,11386],{},"Reed-Solomon correction does not paint missing modules back in by looking at their neighbours. The decoder extracts the codewords it can read, then uses the check values to detect and correct errors within the capacity of each block. A higher level spends more of the symbol on redundancy and leaves less room for payload at the same version. The generator may therefore choose a larger version, producing a denser matrix.",[11,11388,11389],{},"A centred logo can work, but Q or H is not permission to cover a chosen percentage automatically. Functional patterns and the quiet zone must remain intact, contrast must stay strong, modules must be large enough, and the exported result should be tested with the cameras and medium used in production.",[11,11391,11392],{},[1068,11393],{"alt":11394,"src":11395},"QR functional patterns are separated from the interleaved data and check-codeword region; the side flow shows damaged codewords recovered with Reed-Solomon parity","\u002Fimages\u002Fblog\u002Finline_qr-code-history.webp",[15,11397,11399],{"id":11398},"from-electronic-kanban-to-the-phone-camera","From electronic kanban to the phone camera",[11,11401,11402],{},"QR Code first spread through manufacturing and logistics: electronic kanban, parts tracking, shipping and product labels. The open specification and standards then let it move into other industries. Denso Wave dates a major consumer shift in Japan to 2002, when mobile phones with built-in QR reading went on sale. Opening a link or coupon no longer required an industrial scanner.",[11,11404,11405],{},"The format later became part of tickets, advertising and payments. The 2020 pandemic did not make QR codes mainstream from nothing, but it expanded an already familiar interaction. Contactless menus, check-in forms and digital certificates were practical because many systems could generate a compatible symbol and an ordinary phone camera could read it.",[11,11407,11408],{},"The factory code became a general interface through more than one clever square pattern. Rapid location, resilient coding, a published specification, a restrained patent policy and compatible implementations all mattered.",[15,11410,11412],{"id":11411},"what-our-generator-verifies","What our generator verifies",[11,11414,11415,11419],{},[38,11416,11418],{"href":11417},"\u002Ftools\u002Fqr-generator","QR Code Generator"," chooses a version for the entered text and selected L, M, Q or H level, then allows the rendering to be styled. It supports solid, linear and radial fills, four module shapes, corner styles, an adjustable quiet zone, transparent output, top and bottom captions, and logos with module clearing, plates, borders and crop choices. The result can be exported as PNG or standalone SVG.",[11,11421,11422,11423,11426],{},"The live scan check does not judge the image by appearance. After each change, the tool takes the rendered QR region, scales it into a ",[49,11424,11425],{},"360 × 360"," test image, decodes it with jsQR and compares the result with the original string. That is useful evidence for one render, not a certificate that every camera and printing condition will work. Transparent output is tested against white, while its real background may be different; screens and print also change scale, glare, colour and sharpness.",[11,11428,11429],{},"The tool includes the reverse operation as well. A user can load an existing QR image and attempt to extract its content locally in the browser. The text, logo and image being decoded are not sent to a server.",[11,11431,11432],{},"A sensible workflow is simple. Begin with enough quiet zone and strong contrast. Add styling and a logo in small steps while watching the live scan check. Test the final exported file, and for print, scan a sample at its real size on several devices. Q or H provides more recovery capacity, but it does not replace that test.",[15,11434,358],{"id":357},[357,11436],{":slug":11437},"[\"qr-generator\",\"visual-cryptography\",\"color-toolbox\"]",{"title":363,"searchDepth":364,"depth":364,"links":11439},[11440,11441,11442,11443,11444,11445,11446],{"id":11251,"depth":364,"text":11252},{"id":11286,"depth":364,"text":11287},{"id":11311,"depth":364,"text":11312},{"id":11332,"depth":364,"text":11333},{"id":11398,"depth":364,"text":11399},{"id":11411,"depth":364,"text":11412},{"id":357,"depth":364,"text":358},"2026-07-22","How Masahiro Hara's team created QR Code, why Denso opened the specification and what its error-correction levels really mean.",[11450,11453,11456,11459],{"q":11451,"a":11452},"Who invented the QR code?","QR Code was developed by a small Denso team led by Masahiro Hara. The project began in 1992 and the new code was announced in 1994. At the time, the team belonged to a division of Denso Corporation; Denso Wave became a separate company when the business was spun off in 2001.",{"q":11454,"a":11455},"Why can a QR code be scanned at an angle or with part of it damaged?","Three large finder patterns help a scanner locate and orient the symbol, while timing and alignment patterns help reconstruct its grid. Reed-Solomon coding repairs damaged data. Level H can restore approximately 30% of codewords, but that does not mean any 30% of the image may be covered. The location and distribution of the damage matter.",{"q":11457,"a":11458},"Are QR codes really free to use?","Standard-compliant QR codes can be used without a licence agreement or royalties to Denso Wave. The company retained its patents but declared that it would not exercise the relevant patent rights against codes conforming to JIS or ISO. This is not a waiver of every patent for every non-standard variation, and the words QR Code remain a registered trademark of Denso Wave.",{"q":11460,"a":11461},"How is a QR code different from a regular barcode?","A linear barcode encodes information along one axis, while a QR code uses a two-dimensional grid and supports several data modes. That allows it to carry more information, but exact capacity depends on the symbol version, encoding mode and error-correction level. More data and redundancy produce a denser or larger symbol.","\u002Fimages\u002Fblog\u002Fblog_qr-code-history.webp",{},"\u002Fblog\u002Fen\u002Fqr-code-history",{"title":11240,"description":11448},"blog\u002Fen\u002Fqr-code-history",[11468,11469,11470,11471,11472,11473],"QR code history","Denso Wave","Masahiro Hara","Reed-Solomon","error correction","QR codes and COVID","YjgRBa3FrTj-1F1hrHOdMeEP_4_BB9k4UjcjecOsLwI",{"id":11476,"title":11477,"body":11478,"date":12772,"description":12773,"extension":376,"faq":12774,"image":12784,"lang":394,"meta":12785,"navigation":396,"path":12786,"published":396,"readTime":1591,"seo":12787,"stem":12788,"tags":12789,"updated":12772,"__hash__":12795},"blog\u002Fblog\u002Fen\u002Ftelegram-web-apps-sells.md","Telegram Web Apps (TWA): How to Launch a Full SaaS Inside Telegram",{"type":8,"value":11479,"toc":12761},[11480,11487,11490,11493,11497,11500,11506,11514,11520,11530,11537,11540,12292,12296,12299,12303,12310,12358,12362,12365,12647,12651,12661,12664,12700,12712,12720,12724,12742,12745,12758],[11,11481,11482,11483,11486],{},"Telegram has transcended its origins as a privacy-focused messaging client to become a powerful, cross-platform runtime environment. The introduction of ",[26,11484,11485],{},"Telegram Web Apps (TWA)",", officially known as Telegram Mini Apps, allows developers to build highly interactive single-page applications (SPAs) that load directly inside the Telegram application shell.",[11,11488,11489],{},"This provides startups and enterprises with immediate, frictionless access to over 900 million active users. By embedding your B2B SaaS, CRM interface, or utility tool inside a Telegram bot, you remove the classic conversion hurdles of app store downloads, desktop logins, and sign-up flows.",[11,11491,11492],{},"In this developer guide, we will analyze the technical architecture of a TWA, implement a secure hash verification check on the backend, and look at frontend integration patterns.",[15,11494,11496],{"id":11495},"the-telegram-mini-app-ecosystem-architecture","The Telegram Mini App Ecosystem Architecture",[11,11498,11499],{},"Unlike traditional web applications, a TWA has a dual parent relationship. The frontend runs in a sandboxed browser component (WebView) controlled by the Telegram client app, while the backend speaks both to the Telegram Bot API and your application database.",[1564,11501,11504],{"className":11502,"code":11503,"language":4815,"meta":363},[4813],"+-----------------------------------------------------------+\n|                      Telegram Client                      |\n|  +-----------------------------------------------------+  |\n|  |                Mini App (WebView UI)                |  |\n|  |     (Uses window.Telegram.WebApp SDK for bridge)    |  |\n|  +--------------------------+--------------------------+  |\n+-----------------------------|-----------------------------+\n       | (initData Query)     | (Secure HTTPS Request)\n       v                      v\n+--------------+       +------------------------------------+\n| Telegram API |       |            Your Backend            |\n|  (Webhooks)  |       |  (Validates initData via HMAC-256) |\n+------+-------+       +-----------------+------------------+\n       |                                 |\n       +----------------> [DB Sync] \u003C----+\n",[49,11505,11503],{"__ignoreMap":363},[15,11507,11509,11510,11513],{"id":11508},"_1-security-first-validating-the-initdata-payload","1. Security First: Validating the ",[49,11511,11512],{},"initData"," Payload",[11,11515,11516],{},[1068,11517],{"alt":11518,"src":11519},"Telegram Mini App initData HMAC-SHA256 validation flow","\u002Fimages\u002Fblog\u002Finline_telegram-web-apps-sells.webp",[11,11521,11522,11523,11526,11527,507],{},"When a user opens your Mini App, Telegram appends a parameter called ",[49,11524,11525],{},"tgWebAppData"," (or raw search parameters) containing user profiles, launch contexts, and a security ",[49,11528,11529],{},"hash",[11,11531,11532,11533,11536],{},"To prevent users from modifying their user IDs or mocking paid subscription privileges, you ",[26,11534,11535],{},"must validate this hash on your backend"," using your Telegram Bot Token as the HMAC key.",[11,11538,11539],{},"Here is the cryptographic validation implementation using Node.js:",[1564,11541,11545],{"className":11542,"code":11543,"language":11544,"meta":363,"style":363},"language-typescript shiki shiki-themes github-dark","import crypto from 'crypto';\n\ninterface TelegramUserData {\n  id: number;\n  first_name: string;\n  last_name?: string;\n  username?: string;\n  language_code?: string;\n  is_premium?: boolean;\n}\n\ninterface ValidationResult {\n  isValid: boolean;\n  user?: TelegramUserData;\n}\n\nexport function verifyTelegramInitData(rawQueryString: string, botToken: string): ValidationResult {\n  const urlParams = new URLSearchParams(rawQueryString);\n  const hash = urlParams.get('hash');\n  \n  if (!hash) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F 1. Sort all incoming parameters alphabetically, excluding the hash itself\n  const keys = Array.from(urlParams.keys()).filter(key => key !== 'hash').sort();\n  \n  \u002F\u002F 2. Re-create the verification data-check string\n  const dataCheckString = keys\n    .map(key => `${key}=${urlParams.get(key)}`)\n    .join('\\n');\n\n  \u002F\u002F 3. Generate the secret cryptographic key\n  \u002F\u002F We use the constant string \"WebAppData\" to sign the bot token first\n  const secretKey = crypto\n    .createHmac('sha256', 'WebAppData')\n    .update(botToken)\n    .digest();\n\n  \u002F\u002F 4. Calculate the expected hash of the sorted string\n  const computedHash = crypto\n    .createHmac('sha256', secretKey)\n    .update(dataCheckString)\n    .digest('hex');\n\n  \u002F\u002F 5. Compare computed signature with the signature sent by the client\n  const isValid = computedHash === hash;\n\n  if (!isValid) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F Parse user data object if validation succeeded\n  try {\n    const userRaw = urlParams.get('user');\n    const user: TelegramUserData = userRaw ? JSON.parse(userRaw) : undefined;\n    return { isValid: true, user };\n  } catch (error) {\n    return { isValid: true };\n  }\n}\n","typescript",[49,11546,11547,11564,11568,11579,11592,11604,11616,11627,11638,11650,11654,11659,11669,11681,11693,11698,11703,11740,11761,11785,11791,11804,11818,11824,11829,11836,11888,11893,11899,11912,11954,11974,11979,11985,11991,12004,12024,12035,12045,12050,12056,12068,12082,12092,12106,12111,12117,12136,12141,12153,12164,12169,12174,12180,12188,12210,12247,12259,12271,12282,12287],{"__ignoreMap":363},[1572,11548,11549,11552,11555,11558,11561],{"class":1574,"line":1575},[1572,11550,5892],{"class":11551},"snl16",[1572,11553,11554],{"class":1638}," crypto ",[1572,11556,11557],{"class":11551},"from",[1572,11559,11560],{"class":1651}," 'crypto'",[1572,11562,11563],{"class":1638},";\n",[1572,11565,11566],{"class":1574,"line":364},[1572,11567,10309],{"emptyLinePlaceholder":396},[1572,11569,11570,11573,11576],{"class":1574,"line":370},[1572,11571,11572],{"class":11551},"interface",[1572,11574,11575],{"class":6311}," TelegramUserData",[1572,11577,11578],{"class":1638}," {\n",[1572,11580,11581,11585,11587,11590],{"class":1574,"line":1591},[1572,11582,11584],{"class":11583},"s9osk","  id",[1572,11586,8342],{"class":11551},[1572,11588,11589],{"class":1644}," number",[1572,11591,11563],{"class":1638},[1572,11593,11594,11597,11599,11602],{"class":1574,"line":1270},[1572,11595,11596],{"class":11583},"  first_name",[1572,11598,8342],{"class":11551},[1572,11600,11601],{"class":1644}," string",[1572,11603,11563],{"class":1638},[1572,11605,11606,11609,11612,11614],{"class":1574,"line":4389},[1572,11607,11608],{"class":11583},"  last_name",[1572,11610,11611],{"class":11551},"?:",[1572,11613,11601],{"class":1644},[1572,11615,11563],{"class":1638},[1572,11617,11618,11621,11623,11625],{"class":1574,"line":742},[1572,11619,11620],{"class":11583},"  username",[1572,11622,11611],{"class":11551},[1572,11624,11601],{"class":1644},[1572,11626,11563],{"class":1638},[1572,11628,11629,11632,11634,11636],{"class":1574,"line":10336},[1572,11630,11631],{"class":11583},"  language_code",[1572,11633,11611],{"class":11551},[1572,11635,11601],{"class":1644},[1572,11637,11563],{"class":1638},[1572,11639,11640,11643,11645,11648],{"class":1574,"line":10342},[1572,11641,11642],{"class":11583},"  is_premium",[1572,11644,11611],{"class":11551},[1572,11646,11647],{"class":1644}," boolean",[1572,11649,11563],{"class":1638},[1572,11651,11652],{"class":1574,"line":10348},[1572,11653,1670],{"class":1638},[1572,11655,11657],{"class":1574,"line":11656},11,[1572,11658,10309],{"emptyLinePlaceholder":396},[1572,11660,11662,11664,11667],{"class":1574,"line":11661},12,[1572,11663,11572],{"class":11551},[1572,11665,11666],{"class":6311}," ValidationResult",[1572,11668,11578],{"class":1638},[1572,11670,11672,11675,11677,11679],{"class":1574,"line":11671},13,[1572,11673,11674],{"class":11583},"  isValid",[1572,11676,8342],{"class":11551},[1572,11678,11647],{"class":1644},[1572,11680,11563],{"class":1638},[1572,11682,11684,11687,11689,11691],{"class":1574,"line":11683},14,[1572,11685,11686],{"class":11583},"  user",[1572,11688,11611],{"class":11551},[1572,11690,11575],{"class":6311},[1572,11692,11563],{"class":1638},[1572,11694,11696],{"class":1574,"line":11695},15,[1572,11697,1670],{"class":1638},[1572,11699,11701],{"class":1574,"line":11700},16,[1572,11702,10309],{"emptyLinePlaceholder":396},[1572,11704,11706,11708,11710,11713,11716,11719,11721,11723,11725,11728,11730,11732,11734,11736,11738],{"class":1574,"line":11705},17,[1572,11707,3790],{"class":11551},[1572,11709,7934],{"class":11551},[1572,11711,11712],{"class":6311}," verifyTelegramInitData",[1572,11714,11715],{"class":1638},"(",[1572,11717,11718],{"class":11583},"rawQueryString",[1572,11720,8342],{"class":11551},[1572,11722,11601],{"class":1644},[1572,11724,673],{"class":1638},[1572,11726,11727],{"class":11583},"botToken",[1572,11729,8342],{"class":11551},[1572,11731,11601],{"class":1644},[1572,11733,219],{"class":1638},[1572,11735,8342],{"class":11551},[1572,11737,11666],{"class":6311},[1572,11739,11578],{"class":1638},[1572,11741,11743,11746,11749,11752,11755,11758],{"class":1574,"line":11742},18,[1572,11744,11745],{"class":11551},"  const",[1572,11747,11748],{"class":1644}," urlParams",[1572,11750,11751],{"class":11551}," =",[1572,11753,11754],{"class":11551}," new",[1572,11756,11757],{"class":6311}," URLSearchParams",[1572,11759,11760],{"class":1638},"(rawQueryString);\n",[1572,11762,11764,11766,11769,11771,11774,11777,11779,11782],{"class":1574,"line":11763},19,[1572,11765,11745],{"class":11551},[1572,11767,11768],{"class":1644}," hash",[1572,11770,11751],{"class":11551},[1572,11772,11773],{"class":1638}," urlParams.",[1572,11775,11776],{"class":6311},"get",[1572,11778,11715],{"class":1638},[1572,11780,11781],{"class":1651},"'hash'",[1572,11783,11784],{"class":1638},");\n",[1572,11786,11788],{"class":1574,"line":11787},20,[1572,11789,11790],{"class":1638},"  \n",[1572,11792,11794,11797,11799,11801],{"class":1574,"line":11793},21,[1572,11795,11796],{"class":11551},"  if",[1572,11798,4110],{"class":1638},[1572,11800,5583],{"class":11551},[1572,11802,11803],{"class":1638},"hash) {\n",[1572,11805,11807,11810,11813,11815],{"class":1574,"line":11806},22,[1572,11808,11809],{"class":11551},"    return",[1572,11811,11812],{"class":1638}," { isValid: ",[1572,11814,3566],{"class":1644},[1572,11816,11817],{"class":1638}," };\n",[1572,11819,11821],{"class":1574,"line":11820},23,[1572,11822,11823],{"class":1638},"  }\n",[1572,11825,11827],{"class":1574,"line":11826},24,[1572,11828,10309],{"emptyLinePlaceholder":396},[1572,11830,11832],{"class":1574,"line":11831},25,[1572,11833,11835],{"class":11834},"sAwPA","  \u002F\u002F 1. Sort all incoming parameters alphabetically, excluding the hash itself\n",[1572,11837,11839,11841,11844,11846,11849,11851,11854,11857,11860,11863,11865,11868,11871,11874,11877,11880,11882,11885],{"class":1574,"line":11838},26,[1572,11840,11745],{"class":11551},[1572,11842,11843],{"class":1644}," keys",[1572,11845,11751],{"class":11551},[1572,11847,11848],{"class":1638}," Array.",[1572,11850,11557],{"class":6311},[1572,11852,11853],{"class":1638},"(urlParams.",[1572,11855,11856],{"class":6311},"keys",[1572,11858,11859],{"class":1638},"()).",[1572,11861,11862],{"class":6311},"filter",[1572,11864,11715],{"class":1638},[1572,11866,11867],{"class":11583},"key",[1572,11869,11870],{"class":11551}," =>",[1572,11872,11873],{"class":1638}," key ",[1572,11875,11876],{"class":11551},"!==",[1572,11878,11879],{"class":1651}," 'hash'",[1572,11881,8399],{"class":1638},[1572,11883,11884],{"class":6311},"sort",[1572,11886,11887],{"class":1638},"();\n",[1572,11889,11891],{"class":1574,"line":11890},27,[1572,11892,11790],{"class":1638},[1572,11894,11896],{"class":1574,"line":11895},28,[1572,11897,11898],{"class":11834},"  \u002F\u002F 2. Re-create the verification data-check string\n",[1572,11900,11902,11904,11907,11909],{"class":1574,"line":11901},29,[1572,11903,11745],{"class":11551},[1572,11905,11906],{"class":1644}," dataCheckString",[1572,11908,11751],{"class":11551},[1572,11910,11911],{"class":1638}," keys\n",[1572,11913,11915,11918,11921,11923,11925,11927,11930,11932,11935,11938,11940,11942,11944,11946,11948,11951],{"class":1574,"line":11914},30,[1572,11916,11917],{"class":1638},"    .",[1572,11919,11920],{"class":6311},"map",[1572,11922,11715],{"class":1638},[1572,11924,11867],{"class":11583},[1572,11926,11870],{"class":11551},[1572,11928,11929],{"class":1651}," `${",[1572,11931,11867],{"class":1638},[1572,11933,11934],{"class":1651},"}=${",[1572,11936,11937],{"class":1638},"urlParams",[1572,11939,507],{"class":1651},[1572,11941,11776],{"class":6311},[1572,11943,11715],{"class":1651},[1572,11945,11867],{"class":1638},[1572,11947,219],{"class":1651},[1572,11949,11950],{"class":1651},"}`",[1572,11952,11953],{"class":1638},")\n",[1572,11955,11957,11959,11962,11964,11967,11970,11972],{"class":1574,"line":11956},31,[1572,11958,11917],{"class":1638},[1572,11960,11961],{"class":6311},"join",[1572,11963,11715],{"class":1638},[1572,11965,11966],{"class":1651},"'",[1572,11968,11969],{"class":1644},"\\n",[1572,11971,11966],{"class":1651},[1572,11973,11784],{"class":1638},[1572,11975,11977],{"class":1574,"line":11976},32,[1572,11978,10309],{"emptyLinePlaceholder":396},[1572,11980,11982],{"class":1574,"line":11981},33,[1572,11983,11984],{"class":11834},"  \u002F\u002F 3. Generate the secret cryptographic key\n",[1572,11986,11988],{"class":1574,"line":11987},34,[1572,11989,11990],{"class":11834},"  \u002F\u002F We use the constant string \"WebAppData\" to sign the bot token first\n",[1572,11992,11994,11996,11999,12001],{"class":1574,"line":11993},35,[1572,11995,11745],{"class":11551},[1572,11997,11998],{"class":1644}," secretKey",[1572,12000,11751],{"class":11551},[1572,12002,12003],{"class":1638}," crypto\n",[1572,12005,12007,12009,12012,12014,12017,12019,12022],{"class":1574,"line":12006},36,[1572,12008,11917],{"class":1638},[1572,12010,12011],{"class":6311},"createHmac",[1572,12013,11715],{"class":1638},[1572,12015,12016],{"class":1651},"'sha256'",[1572,12018,673],{"class":1638},[1572,12020,12021],{"class":1651},"'WebAppData'",[1572,12023,11953],{"class":1638},[1572,12025,12027,12029,12032],{"class":1574,"line":12026},37,[1572,12028,11917],{"class":1638},[1572,12030,12031],{"class":6311},"update",[1572,12033,12034],{"class":1638},"(botToken)\n",[1572,12036,12038,12040,12043],{"class":1574,"line":12037},38,[1572,12039,11917],{"class":1638},[1572,12041,12042],{"class":6311},"digest",[1572,12044,11887],{"class":1638},[1572,12046,12048],{"class":1574,"line":12047},39,[1572,12049,10309],{"emptyLinePlaceholder":396},[1572,12051,12053],{"class":1574,"line":12052},40,[1572,12054,12055],{"class":11834},"  \u002F\u002F 4. Calculate the expected hash of the sorted string\n",[1572,12057,12059,12061,12064,12066],{"class":1574,"line":12058},41,[1572,12060,11745],{"class":11551},[1572,12062,12063],{"class":1644}," computedHash",[1572,12065,11751],{"class":11551},[1572,12067,12003],{"class":1638},[1572,12069,12071,12073,12075,12077,12079],{"class":1574,"line":12070},42,[1572,12072,11917],{"class":1638},[1572,12074,12011],{"class":6311},[1572,12076,11715],{"class":1638},[1572,12078,12016],{"class":1651},[1572,12080,12081],{"class":1638},", secretKey)\n",[1572,12083,12085,12087,12089],{"class":1574,"line":12084},43,[1572,12086,11917],{"class":1638},[1572,12088,12031],{"class":6311},[1572,12090,12091],{"class":1638},"(dataCheckString)\n",[1572,12093,12095,12097,12099,12101,12104],{"class":1574,"line":12094},44,[1572,12096,11917],{"class":1638},[1572,12098,12042],{"class":6311},[1572,12100,11715],{"class":1638},[1572,12102,12103],{"class":1651},"'hex'",[1572,12105,11784],{"class":1638},[1572,12107,12109],{"class":1574,"line":12108},45,[1572,12110,10309],{"emptyLinePlaceholder":396},[1572,12112,12114],{"class":1574,"line":12113},46,[1572,12115,12116],{"class":11834},"  \u002F\u002F 5. Compare computed signature with the signature sent by the client\n",[1572,12118,12120,12122,12125,12127,12130,12133],{"class":1574,"line":12119},47,[1572,12121,11745],{"class":11551},[1572,12123,12124],{"class":1644}," isValid",[1572,12126,11751],{"class":11551},[1572,12128,12129],{"class":1638}," computedHash ",[1572,12131,12132],{"class":11551},"===",[1572,12134,12135],{"class":1638}," hash;\n",[1572,12137,12139],{"class":1574,"line":12138},48,[1572,12140,10309],{"emptyLinePlaceholder":396},[1572,12142,12144,12146,12148,12150],{"class":1574,"line":12143},49,[1572,12145,11796],{"class":11551},[1572,12147,4110],{"class":1638},[1572,12149,5583],{"class":11551},[1572,12151,12152],{"class":1638},"isValid) {\n",[1572,12154,12156,12158,12160,12162],{"class":1574,"line":12155},50,[1572,12157,11809],{"class":11551},[1572,12159,11812],{"class":1638},[1572,12161,3566],{"class":1644},[1572,12163,11817],{"class":1638},[1572,12165,12167],{"class":1574,"line":12166},51,[1572,12168,11823],{"class":1638},[1572,12170,12172],{"class":1574,"line":12171},52,[1572,12173,10309],{"emptyLinePlaceholder":396},[1572,12175,12177],{"class":1574,"line":12176},53,[1572,12178,12179],{"class":11834},"  \u002F\u002F Parse user data object if validation succeeded\n",[1572,12181,12183,12186],{"class":1574,"line":12182},54,[1572,12184,12185],{"class":11551},"  try",[1572,12187,11578],{"class":1638},[1572,12189,12191,12194,12197,12199,12201,12203,12205,12208],{"class":1574,"line":12190},55,[1572,12192,12193],{"class":11551},"    const",[1572,12195,12196],{"class":1644}," userRaw",[1572,12198,11751],{"class":11551},[1572,12200,11773],{"class":1638},[1572,12202,11776],{"class":6311},[1572,12204,11715],{"class":1638},[1572,12206,12207],{"class":1651},"'user'",[1572,12209,11784],{"class":1638},[1572,12211,12213,12215,12218,12220,12222,12224,12227,12229,12232,12234,12237,12240,12242,12245],{"class":1574,"line":12212},56,[1572,12214,12193],{"class":11551},[1572,12216,12217],{"class":1644}," user",[1572,12219,8342],{"class":11551},[1572,12221,11575],{"class":6311},[1572,12223,11751],{"class":11551},[1572,12225,12226],{"class":1638}," userRaw ",[1572,12228,6735],{"class":11551},[1572,12230,12231],{"class":1644}," JSON",[1572,12233,507],{"class":1638},[1572,12235,12236],{"class":6311},"parse",[1572,12238,12239],{"class":1638},"(userRaw) ",[1572,12241,8342],{"class":11551},[1572,12243,12244],{"class":1644}," undefined",[1572,12246,11563],{"class":1638},[1572,12248,12250,12252,12254,12256],{"class":1574,"line":12249},57,[1572,12251,11809],{"class":11551},[1572,12253,11812],{"class":1638},[1572,12255,3638],{"class":1644},[1572,12257,12258],{"class":1638},", user };\n",[1572,12260,12262,12265,12268],{"class":1574,"line":12261},58,[1572,12263,12264],{"class":1638},"  } ",[1572,12266,12267],{"class":11551},"catch",[1572,12269,12270],{"class":1638}," (error) {\n",[1572,12272,12274,12276,12278,12280],{"class":1574,"line":12273},59,[1572,12275,11809],{"class":11551},[1572,12277,11812],{"class":1638},[1572,12279,3638],{"class":1644},[1572,12281,11817],{"class":1638},[1572,12283,12285],{"class":1574,"line":12284},60,[1572,12286,11823],{"class":1638},[1572,12288,12290],{"class":1574,"line":12289},61,[1572,12291,1670],{"class":1638},[15,12293,12295],{"id":12294},"_2-frontend-integration-theme-synchronization","2. Frontend Integration & Theme Synchronization",[11,12297,12298],{},"To deliver a premium UI\u002FUX, your Mini App should visually blend with the Telegram client’s dark\u002Flight settings. You can access the styles and control client-side behaviors using the official Telegram WebApp JS library.",[70,12300,12302],{"id":12301},"step-1-include-the-script-in-nuxt-3-html","Step 1: Include the Script in Nuxt 3 \u002F HTML",[11,12304,12305,12306,12309],{},"Add the official script to your page header or use the ",[49,12307,12308],{},"useHead"," composable in Nuxt:",[1564,12311,12313],{"className":11542,"code":12312,"language":11544,"meta":363,"style":363},"\u002F\u002F app.vue or page layout\nuseHead({\n  script: [\n    { src: 'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js', defer: true }\n  ]\n})\n",[49,12314,12315,12320,12327,12332,12348,12353],{"__ignoreMap":363},[1572,12316,12317],{"class":1574,"line":1575},[1572,12318,12319],{"class":11834},"\u002F\u002F app.vue or page layout\n",[1572,12321,12322,12324],{"class":1574,"line":364},[1572,12323,12308],{"class":6311},[1572,12325,12326],{"class":1638},"({\n",[1572,12328,12329],{"class":1574,"line":370},[1572,12330,12331],{"class":1638},"  script: [\n",[1572,12333,12334,12337,12340,12343,12345],{"class":1574,"line":1591},[1572,12335,12336],{"class":1638},"    { src: ",[1572,12338,12339],{"class":1651},"'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js'",[1572,12341,12342],{"class":1638},", defer: ",[1572,12344,3638],{"class":1644},[1572,12346,12347],{"class":1638}," }\n",[1572,12349,12350],{"class":1574,"line":1270},[1572,12351,12352],{"class":1638},"  ]\n",[1572,12354,12355],{"class":1574,"line":4389},[1572,12356,12357],{"class":1638},"})\n",[70,12359,12361],{"id":12360},"step-2-access-the-webapp-bridge-in-vue","Step 2: Access the WebApp Bridge in Vue",[11,12363,12364],{},"Create a Vue composable to access and synchronize Telegram styles:",[1564,12366,12368],{"className":11542,"code":12367,"language":11544,"meta":363,"style":363},"\u002F\u002F composables\u002FuseTelegram.ts\nimport { ref, onMounted } from 'vue';\n\nexport function useTelegram() {\n  const isReady = ref(false);\n  const user = ref\u003Cany>(null);\n\n  onMounted(() => {\n    const tg = (window as any).Telegram?.WebApp;\n    if (tg) {\n      tg.ready();\n      tg.expand(); \u002F\u002F Request the container to fill maximum vertical space\n      \n      user.value = tg.initDataUnsafe?.user;\n      isReady.value = true;\n\n      \u002F\u002F Apply Telegram theme colors to CSS custom properties\n      const root = document.documentElement;\n      root.style.setProperty('--color-tg-bg', tg.themeParams.bg_color);\n      root.style.setProperty('--color-tg-text', tg.themeParams.text_color);\n      root.style.setProperty('--color-tg-button', tg.themeParams.button_color);\n      root.style.setProperty('--color-tg-button-text', tg.themeParams.button_text_color);\n    }\n  });\n\n  return { isReady, user };\n}\n",[49,12369,12370,12375,12389,12393,12405,12423,12445,12449,12462,12483,12491,12501,12514,12519,12529,12541,12545,12550,12563,12579,12593,12607,12621,12626,12631,12635,12643],{"__ignoreMap":363},[1572,12371,12372],{"class":1574,"line":1575},[1572,12373,12374],{"class":11834},"\u002F\u002F composables\u002FuseTelegram.ts\n",[1572,12376,12377,12379,12382,12384,12387],{"class":1574,"line":364},[1572,12378,5892],{"class":11551},[1572,12380,12381],{"class":1638}," { ref, onMounted } ",[1572,12383,11557],{"class":11551},[1572,12385,12386],{"class":1651}," 'vue'",[1572,12388,11563],{"class":1638},[1572,12390,12391],{"class":1574,"line":370},[1572,12392,10309],{"emptyLinePlaceholder":396},[1572,12394,12395,12397,12399,12402],{"class":1574,"line":1591},[1572,12396,3790],{"class":11551},[1572,12398,7934],{"class":11551},[1572,12400,12401],{"class":6311}," useTelegram",[1572,12403,12404],{"class":1638},"() {\n",[1572,12406,12407,12409,12412,12414,12417,12419,12421],{"class":1574,"line":1270},[1572,12408,11745],{"class":11551},[1572,12410,12411],{"class":1644}," isReady",[1572,12413,11751],{"class":11551},[1572,12415,12416],{"class":6311}," ref",[1572,12418,11715],{"class":1638},[1572,12420,3566],{"class":1644},[1572,12422,11784],{"class":1638},[1572,12424,12425,12427,12429,12431,12433,12435,12438,12441,12443],{"class":1574,"line":4389},[1572,12426,11745],{"class":11551},[1572,12428,12217],{"class":1644},[1572,12430,11751],{"class":11551},[1572,12432,12416],{"class":6311},[1572,12434,6305],{"class":1638},[1572,12436,12437],{"class":1644},"any",[1572,12439,12440],{"class":1638},">(",[1572,12442,1741],{"class":1644},[1572,12444,11784],{"class":1638},[1572,12446,12447],{"class":1574,"line":742},[1572,12448,10309],{"emptyLinePlaceholder":396},[1572,12450,12451,12454,12457,12460],{"class":1574,"line":10336},[1572,12452,12453],{"class":6311},"  onMounted",[1572,12455,12456],{"class":1638},"(() ",[1572,12458,12459],{"class":11551},"=>",[1572,12461,11578],{"class":1638},[1572,12463,12464,12466,12469,12471,12474,12477,12480],{"class":1574,"line":10342},[1572,12465,12193],{"class":11551},[1572,12467,12468],{"class":1644}," tg",[1572,12470,11751],{"class":11551},[1572,12472,12473],{"class":1638}," (window ",[1572,12475,12476],{"class":11551},"as",[1572,12478,12479],{"class":1644}," any",[1572,12481,12482],{"class":1638},").Telegram?.WebApp;\n",[1572,12484,12485,12488],{"class":1574,"line":10348},[1572,12486,12487],{"class":11551},"    if",[1572,12489,12490],{"class":1638}," (tg) {\n",[1572,12492,12493,12496,12499],{"class":1574,"line":11656},[1572,12494,12495],{"class":1638},"      tg.",[1572,12497,12498],{"class":6311},"ready",[1572,12500,11887],{"class":1638},[1572,12502,12503,12505,12508,12511],{"class":1574,"line":11661},[1572,12504,12495],{"class":1638},[1572,12506,12507],{"class":6311},"expand",[1572,12509,12510],{"class":1638},"(); ",[1572,12512,12513],{"class":11834},"\u002F\u002F Request the container to fill maximum vertical space\n",[1572,12515,12516],{"class":1574,"line":11671},[1572,12517,12518],{"class":1638},"      \n",[1572,12520,12521,12524,12526],{"class":1574,"line":11683},[1572,12522,12523],{"class":1638},"      user.value ",[1572,12525,6315],{"class":11551},[1572,12527,12528],{"class":1638}," tg.initDataUnsafe?.user;\n",[1572,12530,12531,12534,12536,12539],{"class":1574,"line":11695},[1572,12532,12533],{"class":1638},"      isReady.value ",[1572,12535,6315],{"class":11551},[1572,12537,12538],{"class":1644}," true",[1572,12540,11563],{"class":1638},[1572,12542,12543],{"class":1574,"line":11700},[1572,12544,10309],{"emptyLinePlaceholder":396},[1572,12546,12547],{"class":1574,"line":11705},[1572,12548,12549],{"class":11834},"      \u002F\u002F Apply Telegram theme colors to CSS custom properties\n",[1572,12551,12552,12555,12558,12560],{"class":1574,"line":11742},[1572,12553,12554],{"class":11551},"      const",[1572,12556,12557],{"class":1644}," root",[1572,12559,11751],{"class":11551},[1572,12561,12562],{"class":1638}," document.documentElement;\n",[1572,12564,12565,12568,12571,12573,12576],{"class":1574,"line":11763},[1572,12566,12567],{"class":1638},"      root.style.",[1572,12569,12570],{"class":6311},"setProperty",[1572,12572,11715],{"class":1638},[1572,12574,12575],{"class":1651},"'--color-tg-bg'",[1572,12577,12578],{"class":1638},", tg.themeParams.bg_color);\n",[1572,12580,12581,12583,12585,12587,12590],{"class":1574,"line":11787},[1572,12582,12567],{"class":1638},[1572,12584,12570],{"class":6311},[1572,12586,11715],{"class":1638},[1572,12588,12589],{"class":1651},"'--color-tg-text'",[1572,12591,12592],{"class":1638},", tg.themeParams.text_color);\n",[1572,12594,12595,12597,12599,12601,12604],{"class":1574,"line":11793},[1572,12596,12567],{"class":1638},[1572,12598,12570],{"class":6311},[1572,12600,11715],{"class":1638},[1572,12602,12603],{"class":1651},"'--color-tg-button'",[1572,12605,12606],{"class":1638},", tg.themeParams.button_color);\n",[1572,12608,12609,12611,12613,12615,12618],{"class":1574,"line":11806},[1572,12610,12567],{"class":1638},[1572,12612,12570],{"class":6311},[1572,12614,11715],{"class":1638},[1572,12616,12617],{"class":1651},"'--color-tg-button-text'",[1572,12619,12620],{"class":1638},", tg.themeParams.button_text_color);\n",[1572,12622,12623],{"class":1574,"line":11820},[1572,12624,12625],{"class":1638},"    }\n",[1572,12627,12628],{"class":1574,"line":11826},[1572,12629,12630],{"class":1638},"  });\n",[1572,12632,12633],{"class":1574,"line":11831},[1572,12634,10309],{"emptyLinePlaceholder":396},[1572,12636,12637,12640],{"class":1574,"line":11838},[1572,12638,12639],{"class":11551},"  return",[1572,12641,12642],{"class":1638}," { isReady, user };\n",[1572,12644,12645],{"class":1574,"line":11890},[1572,12646,1670],{"class":1638},[15,12648,12650],{"id":12649},"_3-monetization-payments-via-telegram-stars","3. Monetization: Payments via Telegram Stars",[11,12652,12653,12654,4110,12657,12660],{},"When operating inside Telegram, all digital services or content purchases must comply with Apple App Store and Google Play policies. Telegram enforces this by requiring the use of ",[26,12655,12656],{},"Telegram Stars",[49,12658,12659],{},"XTR",") for digital goods.",[11,12662,12663],{},"The flow for receiving Stars payments:",[4001,12665,12666,12672,12684,12690],{},[301,12667,12668,12671],{},[26,12669,12670],{},"Request Invoice",": The TWA requests the backend to generate an invoice.",[301,12673,12674,12677,12678,12681,12682,507],{},[26,12675,12676],{},"Bot Sends Invoice",": The backend calls Bot API ",[49,12679,12680],{},"sendInvoice"," using the currency ",[49,12683,12659],{},[301,12685,12686,12689],{},[26,12687,12688],{},"Client Checkout",": The Telegram client opens a native overlay allowing the user to pay using Stars purchased in-app.",[301,12691,12692,12695,12696,12699],{},[26,12693,12694],{},"Verification",": Telegram sends a webhook ",[49,12697,12698],{},"successful_payment"," to your bot, which credits the user's account in your DB.",[11,12701,12702,12703,12707,12708,507],{},"In my Telegram bot hosting platform, ",[38,12704,12706],{"href":12705},"\u002Fprojects\u002Ftelego","TeleGo.io",", we provide exactly these billing options, allowing users to spin up their own TWAs and receive Stars instantly. Additionally, we support hybrid human-AI helpdesks, which you can learn to build in my guide on ",[38,12709,12711],{"href":12710},"\u002Fblog\u002Ftelegram-bot-ai-rag-support","RAG AI Customer Support Bots",[11,12713,12714,12715,12719],{},"For physical goods and consulting services, traditional gateways can be used. Read my ",[38,12716,12718],{"href":12717},"\u002Fblog\u002Fsaas-stripe-billing-integration","SaaS Stripe Billing Integration guide"," for Express\u002FTypeScript templates.",[15,12721,12723],{"id":12722},"sources-and-documentation","Sources and documentation",[298,12725,12726,12734],{},[301,12727,12728,12733],{},[38,12729,12732],{"href":12730,"rel":12731},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps",[429],"Telegram Mini Apps"," — the official WebApp SDK and initData documentation",[301,12735,12736,12741],{},[38,12737,12740],{"href":12738,"rel":12739},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments",[429],"Bot Payments API"," — invoices, Telegram Stars, and payment webhooks",[11,12743,12744],{},"By combining native web frameworks with the Telegram WebApp API, developers can ship complex SaaS platforms and digital products directly into active messaging channels.",[11,12746,12747,12748,12752,12753,12757],{},"If you are planning to build a high-performance Telegram Mini App with secure billing integrations and dynamic frontend design, check out my ",[38,12749,12751],{"href":12750},"\u002Ftelegram-bots","Telegram Bot Development Service"," or request a ",[38,12754,12756],{"href":12755},"\u002Fconsultations","Technical Architecture Consultation"," to get a production blueprint.",[1779,12759,12760],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":12762},[12763,12764,12766,12770,12771],{"id":11495,"depth":364,"text":11496},{"id":11508,"depth":364,"text":12765},"1. Security First: Validating the initData Payload",{"id":12294,"depth":364,"text":12295,"children":12767},[12768,12769],{"id":12301,"depth":370,"text":12302},{"id":12360,"depth":370,"text":12361},{"id":12649,"depth":364,"text":12650},{"id":12722,"depth":364,"text":12723},"2026-07-05","A comprehensive developer's guide to using Telegram Mini Apps as a dynamic web frontend, implementing secure hash validation, and processing in-app purchases.",[12775,12778,12781],{"q":12776,"a":12777},"How is a Telegram Mini App different from a regular bot?","A bot communicates through messages and buttons, while a Mini App opens a full web interface inside Telegram: dashboards, catalogs, forms, charts. It's an SPA running in a WebView with its own frontend, and authorization happens automatically through the Telegram account.",{"q":12779,"a":12780},"How do I verify that user data in a Mini App isn't forged?","Through cryptographic initData validation: Telegram signs the user data with an HMAC-SHA256 signature derived from the bot token. The backend must recompute and compare the signature on every request — without it, anyone can impersonate any user.",{"q":12782,"a":12783},"When are Telegram Stars mandatory, and when can I use Stripe?","Digital goods and subscriptions inside a Mini App must be paid with Telegram Stars per Apple and Google policies. Physical goods, services, and consulting can be sold through classic gateways like Stripe.","\u002Fimages\u002Fblog\u002Fblog_telegram_twa.webp",{},"\u002Fblog\u002Fen\u002Ftelegram-web-apps-sells",{"title":11477,"description":12773},"blog\u002Fen\u002Ftelegram-web-apps-sells",[12790,12791,12792,12793,12794],"Telegram","Mini Apps","SaaS","Node.js","Frontend","DoyArePPmLLPeMJ9SmYAcxviaXQBZhZHzdKtdlk5oMY",{"id":12797,"title":12798,"body":12799,"date":13349,"description":13350,"extension":376,"faq":13351,"image":13361,"lang":394,"meta":13362,"navigation":396,"path":13363,"published":396,"readTime":1591,"seo":13364,"stem":13365,"tags":13366,"updated":12772,"__hash__":13371},"blog\u002Fblog\u002Fen\u002Fgenerative-engine-optimization-geo.md","Generative Engine Optimization (GEO): The New SEO for AI Search",{"type":8,"value":12800,"toc":13338},[12801,12804,12807,12813,12816,12820,12823,12829,12832,12836,12843,12846,12939,12946,12950,12954,12957,12971,12975,12978,12992,12996,12999,13003,13006,13274,13285,13292,13294,13320,13323,13335],[11,12802,12803],{},"The web search ecosystem is undergoing its most revolutionary change since Google’s inception in 1998. Traditional search engine optimization (SEO) is built on ranking links on a Search Engine Results Page (SERP). Today, however, millions of users are bypassing search result pages altogether. They query conversational AI assistants—such as ChatGPT Search, Perplexity, Gemini, and Claude—to get direct, synthesized answers.",[11,12805,12806],{},"If your product, SaaS, or agency is not recommended and cited in these AI responses, your business is virtually invisible to a rapidly growing demographic of buyers.",[11,12808,12809,12810,507],{},"To stay competitive, you must optimize your site for large language model (LLM) search engines. This is the discipline of ",[26,12811,12812],{},"Generative Engine Optimization (GEO)",[11,12814,12815],{},"In this deep dive, we will explore how conversational search engines work, analyze academic ranking factors, and implement technical adjustments to secure your brand's placement in AI summaries.",[15,12817,12819],{"id":12818},"the-ai-search-rag-retrieval-pipeline","The AI Search (RAG) Retrieval Pipeline",[11,12821,12822],{},"Unlike classic crawlers that map keywords to index scores, AI search engines use a hybrid retrieval pipeline:",[1564,12824,12827],{"className":12825,"code":12826,"language":4815,"meta":363},[4813],"[User asks: \"Best multi-tenant CRM for real estate\"]\n                             │\n                             v\n           (AI Query Understanding & Search)\n    LLM rewrites query -> Runs web search (Bing\u002FGoogle API)\n                             │\n                             v\n           (Scraping & Embedding top 10 results)\n     AI scrapers read HTML -> Vectorize content chunks\n                             │\n                             v\n            (RAG Re-ranking & Synthesis)\n     Matches question vector against scraped chunks ->\n  Selects top 3 sources -> Synthesizes single answer with links\n                             │\n                             v\n[Output: \"I recommend AcmeCRM [1] because of its postgres schema isolation [2]\"]\n",[49,12828,12826],{"__ignoreMap":363},[11,12830,12831],{},"To rank in this ecosystem, your content must satisfy two criteria: it must appear in the top search indexes, and it must be structured in a way that the LLM re-ranker selects it as the most authoritative, easy-to-summarize source.",[15,12833,12835],{"id":12834},"_1-key-geo-optimization-techniques-backed-by-research","1. Key GEO Optimization Techniques (Backed by Research)",[11,12837,12838,12839,12842],{},"In 2023, researchers from Princeton, Georgia Tech, and the Allen Institute for AI published a landmark study titled ",[1321,12840,12841],{},"“Generative Engine Optimization”",". They systematically tested various content modification techniques to see what factors influence an LLM’s probability of citing a website.",[11,12844,12845],{},"Here is the data-backed rubric of what works best:",[75,12847,12848,12862],{},[78,12849,12850],{},[81,12851,12852,12856,12859],{},[84,12853,12855],{"align":12854},"left","GEO Strategy",[84,12857,12858],{"align":12854},"What it means",[84,12860,12861],{"align":12854},"Citation Increase",[91,12863,12864,12879,12894,12909,12924],{},[81,12865,12866,12871,12874],{},[96,12867,12868],{"align":12854},[26,12869,12870],{},"Cite Sources",[96,12872,12873],{"align":12854},"Adding outbound links to high-authority academic papers, government stats, or news portals.",[96,12875,12876],{"align":12854},[26,12877,12878],{},"+40.3%",[81,12880,12881,12886,12889],{},[96,12882,12883],{"align":12854},[26,12884,12885],{},"Include Statistics",[96,12887,12888],{"align":12854},"Replacing vague claims with verified quantitative data and numerical metrics.",[96,12890,12891],{"align":12854},[26,12892,12893],{},"+37.5%",[81,12895,12896,12901,12904],{},[96,12897,12898],{"align":12854},[26,12899,12900],{},"Information Gain",[96,12902,12903],{"align":12854},"Adding unique data or insights not present in other top search results.",[96,12905,12906],{"align":12854},[26,12907,12908],{},"+32.1%",[81,12910,12911,12916,12919],{},[96,12912,12913],{"align":12854},[26,12914,12915],{},"Q&A Formatting",[96,12917,12918],{"align":12854},"Structuring headlines as questions followed by a direct, concise answer.",[96,12920,12921],{"align":12854},[26,12922,12923],{},"+25.8%",[81,12925,12926,12931,12934],{},[96,12927,12928],{"align":12854},[26,12929,12930],{},"Markdown Tables",[96,12932,12933],{"align":12854},"Organizing complex comparisons in clean markdown tables.",[96,12935,12936],{"align":12854},[26,12937,12938],{},"+18.2%",[11,12940,12941,12942,12945],{},"Traditional SEO tactics like keyword stuffing actually ",[1321,12943,12944],{},"decrease"," citation scores in LLM re-rankers because they reduce readability and lower the model's summarization score.",[15,12947,12949],{"id":12948},"_2-practical-technical-geo-checklist","2. Practical Technical GEO Checklist",[70,12951,12953],{"id":12952},"a-implement-qa-style-h2h3-headlines","A. Implement Q&A-Style H2\u002FH3 Headlines",[11,12955,12956],{},"AI models are trained to map answers to user queries. If a user asks a question, the LLM will scan search results for exact matching headers.",[298,12958,12959,12965],{},[301,12960,12961,12964],{},[1321,12962,12963],{},"Incorrect",": \"Our Billing Infrastructure Customizations\"",[301,12966,12967,12970],{},[1321,12968,12969],{},"Correct",": \"How does the platform handle subscription billing?\" Follow this header immediately with a clear, concise 2-sentence summary of your architecture, followed by deeper details.",[70,12972,12974],{"id":12973},"b-add-outbound-citation-anchors","B. Add Outbound Citation Anchors",[11,12976,12977],{},"When referencing industry standards, link to authoritative repositories.",[298,12979,12980],{},[301,12981,12982,12985,12986,12991],{},[1321,12983,12984],{},"Example",": \"According to the ",[38,12987,12990],{"href":12988,"rel":12989},"https:\u002F\u002Fowasp.org\u002Fwww-project-top-ten\u002F",[429],"OWASP Top 10 Security Standards",", input sanitization is critical to prevent injection attacks...\"\nThis outbound link signals high authority to the LLM re-ranker.",[70,12993,12995],{"id":12994},"c-optimize-for-readability-flesch-kincaid-score","C. Optimize for Readability (Flesch-Kincaid Score)",[11,12997,12998],{},"LLMs prefer texts that are easy to parse and compile. Use clear, active voice, short sentences, and structured bullet points. Avoid flowery promotional adjectives.",[15,13000,13002],{"id":13001},"_3-feeding-clean-data-to-ai-bots-using-json-ld","3. Feeding Clean Data to AI bots using JSON-LD",[11,13004,13005],{},"To remove all ambiguity about your services, pricing, and entity relations, output clean semantic structures. Here is a JSON-LD snippet for a consultancy service that should be dynamically injected into your site's header:",[1564,13007,13009],{"className":1629,"code":13008,"language":1631,"meta":363,"style":363},"{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"ConsultingService\",\n  \"@id\": \"https:\u002F\u002Falexrel.com\u002Fconsultations#service\",\n  \"name\": \"Technical Architecture Consultation\",\n  \"description\": \"Professional codebase audits, database scaling reviews, and SaaS MVP planning.\",\n  \"provider\": {\n    \"@type\": \"Person\",\n    \"name\": \"Alex Rel\",\n    \"jobTitle\": \"Senior Fullstack Developer\",\n    \"url\": \"https:\u002F\u002Falexrel.com\"\n  },\n  \"offers\": {\n    \"@type\": \"Offer\",\n    \"priceCurrency\": \"USD\",\n    \"price\": \"250\",\n    \"priceSpecification\": {\n      \"@type\": \"UnitPriceSpecification\",\n      \"priceType\": \"https:\u002F\u002Fschema.org\u002FRecurringPrice\",\n      \"billingIncrement\": 1,\n      \"referenceQuantity\": {\n        \"@type\": \"QuantitativeValue\",\n        \"value\": 1,\n        \"unitCode\": \"HUR\"\n      }\n    }\n  }\n}\n",[49,13010,13011,13015,13027,13039,13051,13062,13074,13082,13094,13106,13118,13128,13133,13140,13151,13163,13175,13182,13194,13206,13217,13224,13236,13247,13257,13262,13266,13270],{"__ignoreMap":363},[1572,13012,13013],{"class":1574,"line":1575},[1572,13014,1639],{"class":1638},[1572,13016,13017,13020,13022,13025],{"class":1574,"line":364},[1572,13018,13019],{"class":1644},"  \"@context\"",[1572,13021,1648],{"class":1638},[1572,13023,13024],{"class":1651},"\"https:\u002F\u002Fschema.org\"",[1572,13026,1655],{"class":1638},[1572,13028,13029,13032,13034,13037],{"class":1574,"line":370},[1572,13030,13031],{"class":1644},"  \"@type\"",[1572,13033,1648],{"class":1638},[1572,13035,13036],{"class":1651},"\"ConsultingService\"",[1572,13038,1655],{"class":1638},[1572,13040,13041,13044,13046,13049],{"class":1574,"line":1591},[1572,13042,13043],{"class":1644},"  \"@id\"",[1572,13045,1648],{"class":1638},[1572,13047,13048],{"class":1651},"\"https:\u002F\u002Falexrel.com\u002Fconsultations#service\"",[1572,13050,1655],{"class":1638},[1572,13052,13053,13055,13057,13060],{"class":1574,"line":1270},[1572,13054,1645],{"class":1644},[1572,13056,1648],{"class":1638},[1572,13058,13059],{"class":1651},"\"Technical Architecture Consultation\"",[1572,13061,1655],{"class":1638},[1572,13063,13064,13067,13069,13072],{"class":1574,"line":4389},[1572,13065,13066],{"class":1644},"  \"description\"",[1572,13068,1648],{"class":1638},[1572,13070,13071],{"class":1651},"\"Professional codebase audits, database scaling reviews, and SaaS MVP planning.\"",[1572,13073,1655],{"class":1638},[1572,13075,13076,13079],{"class":1574,"line":742},[1572,13077,13078],{"class":1644},"  \"provider\"",[1572,13080,13081],{"class":1638},": {\n",[1572,13083,13084,13087,13089,13092],{"class":1574,"line":10336},[1572,13085,13086],{"class":1644},"    \"@type\"",[1572,13088,1648],{"class":1638},[1572,13090,13091],{"class":1651},"\"Person\"",[1572,13093,1655],{"class":1638},[1572,13095,13096,13099,13101,13104],{"class":1574,"line":10342},[1572,13097,13098],{"class":1644},"    \"name\"",[1572,13100,1648],{"class":1638},[1572,13102,13103],{"class":1651},"\"Alex Rel\"",[1572,13105,1655],{"class":1638},[1572,13107,13108,13111,13113,13116],{"class":1574,"line":10348},[1572,13109,13110],{"class":1644},"    \"jobTitle\"",[1572,13112,1648],{"class":1638},[1572,13114,13115],{"class":1651},"\"Senior Fullstack Developer\"",[1572,13117,1655],{"class":1638},[1572,13119,13120,13123,13125],{"class":1574,"line":11656},[1572,13121,13122],{"class":1644},"    \"url\"",[1572,13124,1648],{"class":1638},[1572,13126,13127],{"class":1651},"\"https:\u002F\u002Falexrel.com\"\n",[1572,13129,13130],{"class":1574,"line":11661},[1572,13131,13132],{"class":1638},"  },\n",[1572,13134,13135,13138],{"class":1574,"line":11671},[1572,13136,13137],{"class":1644},"  \"offers\"",[1572,13139,13081],{"class":1638},[1572,13141,13142,13144,13146,13149],{"class":1574,"line":11683},[1572,13143,13086],{"class":1644},[1572,13145,1648],{"class":1638},[1572,13147,13148],{"class":1651},"\"Offer\"",[1572,13150,1655],{"class":1638},[1572,13152,13153,13156,13158,13161],{"class":1574,"line":11695},[1572,13154,13155],{"class":1644},"    \"priceCurrency\"",[1572,13157,1648],{"class":1638},[1572,13159,13160],{"class":1651},"\"USD\"",[1572,13162,1655],{"class":1638},[1572,13164,13165,13168,13170,13173],{"class":1574,"line":11700},[1572,13166,13167],{"class":1644},"    \"price\"",[1572,13169,1648],{"class":1638},[1572,13171,13172],{"class":1651},"\"250\"",[1572,13174,1655],{"class":1638},[1572,13176,13177,13180],{"class":1574,"line":11705},[1572,13178,13179],{"class":1644},"    \"priceSpecification\"",[1572,13181,13081],{"class":1638},[1572,13183,13184,13187,13189,13192],{"class":1574,"line":11742},[1572,13185,13186],{"class":1644},"      \"@type\"",[1572,13188,1648],{"class":1638},[1572,13190,13191],{"class":1651},"\"UnitPriceSpecification\"",[1572,13193,1655],{"class":1638},[1572,13195,13196,13199,13201,13204],{"class":1574,"line":11763},[1572,13197,13198],{"class":1644},"      \"priceType\"",[1572,13200,1648],{"class":1638},[1572,13202,13203],{"class":1651},"\"https:\u002F\u002Fschema.org\u002FRecurringPrice\"",[1572,13205,1655],{"class":1638},[1572,13207,13208,13211,13213,13215],{"class":1574,"line":11787},[1572,13209,13210],{"class":1644},"      \"billingIncrement\"",[1572,13212,1648],{"class":1638},[1572,13214,9128],{"class":1644},[1572,13216,1655],{"class":1638},[1572,13218,13219,13222],{"class":1574,"line":11793},[1572,13220,13221],{"class":1644},"      \"referenceQuantity\"",[1572,13223,13081],{"class":1638},[1572,13225,13226,13229,13231,13234],{"class":1574,"line":11806},[1572,13227,13228],{"class":1644},"        \"@type\"",[1572,13230,1648],{"class":1638},[1572,13232,13233],{"class":1651},"\"QuantitativeValue\"",[1572,13235,1655],{"class":1638},[1572,13237,13238,13241,13243,13245],{"class":1574,"line":11820},[1572,13239,13240],{"class":1644},"        \"value\"",[1572,13242,1648],{"class":1638},[1572,13244,9128],{"class":1644},[1572,13246,1655],{"class":1638},[1572,13248,13249,13252,13254],{"class":1574,"line":11826},[1572,13250,13251],{"class":1644},"        \"unitCode\"",[1572,13253,1648],{"class":1638},[1572,13255,13256],{"class":1651},"\"HUR\"\n",[1572,13258,13259],{"class":1574,"line":11831},[1572,13260,13261],{"class":1638},"      }\n",[1572,13263,13264],{"class":1574,"line":11838},[1572,13265,12625],{"class":1638},[1572,13267,13268],{"class":1574,"line":11890},[1572,13269,11823],{"class":1638},[1572,13271,13272],{"class":1574,"line":11895},[1572,13273,1670],{"class":1638},[11,13275,13276,13277,1159,13280,13284],{},"When optimizing my own projects, such as the ",[38,13278,13279],{"href":12705},"TeleGo.io platform",[38,13281,13283],{"href":13282},"\u002Fprojects\u002Flingvohabit","LingvoHabit language portal",", I implemented these precise structural adjustments. Today, both services are successfully cataloged and actively cited in real-time ChatGPT Search and Perplexity queries.",[11,13286,13287,13288,507],{},"To learn how to configure your robots.txt and schema structures to facilitate this, read my technical guide on ",[38,13289,13291],{"href":13290},"\u002Fblog\u002Fwebsite-optimization-ai-crawlers","Optimizing your Website for AI Crawlers: Robots.txt & JSON-LD",[15,13293,12723],{"id":12722},[298,13295,13296,13304,13312],{},[301,13297,13298,13303],{},[38,13299,13302],{"href":13300,"rel":13301},"https:\u002F\u002Farxiv.org\u002Fabs\u002F2311.09735",[429],"GEO: Generative Engine Optimization"," — the original Princeton \u002F Georgia Tech \u002F Allen AI study behind the strategy table above",[301,13305,13306,13311],{},[38,13307,13310],{"href":13308,"rel":13309},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs",[429],"Google Search Central"," — official documentation on indexing and structured data",[301,13313,13314,13319],{},[38,13315,13318],{"href":13316,"rel":13317},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fbots",[429],"OpenAI crawler documentation"," — how GPTBot and OAI-SearchBot scan websites",[11,13321,13322],{},"By structuring your content for the Generative Web today, you secure your brand's presence in the AI search portals of tomorrow.",[11,13324,13325,13326,13330,13331,13334],{},"If you want to implement GEO strategies, perform an AI search audit of your website, or optimize your content for ChatGPT and Perplexity, explore my ",[38,13327,13329],{"href":13328},"\u002Fgeo-seo","GEO & SEO Optimization Services"," or book a ",[38,13332,13333],{"href":12755},"Technical Consultation"," to get started.",[1779,13336,13337],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":13339},[13340,13341,13342,13347,13348],{"id":12818,"depth":364,"text":12819},{"id":12834,"depth":364,"text":12835},{"id":12948,"depth":364,"text":12949,"children":13343},[13344,13345,13346],{"id":12952,"depth":370,"text":12953},{"id":12973,"depth":370,"text":12974},{"id":12994,"depth":370,"text":12995},{"id":13001,"depth":364,"text":13002},{"id":12722,"depth":364,"text":12723},"2026-07-02","A technical guide on Generative Engine Optimization (GEO) strategies to improve brand citation and recommendation frequencies in ChatGPT, Perplexity, and Gemini.",[13352,13355,13358],{"q":13353,"a":13354},"What is GEO in simple terms?","GEO (Generative Engine Optimization) is optimizing a website so AI engines — ChatGPT, Perplexity, Gemini — can find, cite, and recommend your brand inside their answers. Where SEO fights for a ranking position, GEO fights for a mention inside the generated answer itself.",{"q":13356,"a":13357},"Does GEO replace classic SEO?","No, they complement each other. AI engines ride on classic search indexes (ChatGPT Search uses Bing, Gemini uses Google), so GEO doesn't work without basic indexability. SEO brings traffic from result pages; GEO brings mentions and clicks from AI answers.",{"q":13359,"a":13360},"Which GEO techniques deliver the biggest lift?","Per the Princeton \u002F Georgia Tech \u002F Allen AI study: citing authoritative sources (+40%), concrete statistics in the text (+37%), unique facts competitors don't have (+32%), and question-and-answer headings (+26%).","\u002Fimages\u002Fblog\u002Fblog_geo_seo.webp",{},"\u002Fblog\u002Fen\u002Fgenerative-engine-optimization-geo",{"title":12798,"description":13350},"blog\u002Fen\u002Fgenerative-engine-optimization-geo",[13367,13368,13369,13370],"GEO","SEO","AI","Marketing","ORbyJ5YAaX6u9CwMCj-9rEdHufZVhvWRBGDpKEF-EpM",{"id":13373,"title":13374,"body":13375,"date":13984,"description":13985,"extension":376,"faq":13986,"image":13996,"lang":394,"meta":13997,"navigation":396,"path":13998,"published":396,"readTime":370,"seo":13999,"stem":14000,"tags":14001,"updated":12772,"__hash__":14004},"blog\u002Fblog\u002Fen\u002Fwebsite-optimization-ai-crawlers.md","Optimizing Your Website for AI Crawlers: Robots.txt & JSON-LD",{"type":8,"value":13376,"toc":13976},[13377,13395,13398,13409,13413,13416,13422,13428,13432,13435,13442,13452,13774,13778,13781,13788,13792,13819,13912,13915,13924,13926,13958,13964,13973],[11,13378,13379,13380,13383,13384,13387,13388,1228,13391,13394],{},"The web scraping landscape has shifted. The scrapers visiting your server are no longer limited to Googlebot, YandexBot, or Bingbot. Today, your server logs are likely dominated by ",[26,13381,13382],{},"GPTBot"," (OpenAI), ",[26,13385,13386],{},"ClaudeBot"," (Anthropic), ",[26,13389,13390],{},"PerplexityBot",[26,13392,13393],{},"OAI-SearchBot"," (OpenAI’s conversational search engine agent).",[11,13396,13397],{},"As these LLM agents actively search the web to answer live user queries, you must ensure that your technical setup permits them to scrape public information, maps your services accurately, and prevents them from reading private database endpoints.",[11,13399,13400,13401,13404,13405,13408],{},"In this technical guide, we will configure an AI-friendly ",[49,13402,13403],{},"robots.txt"," policy, implement nested ",[49,13406,13407],{},"JSON-LD"," schemas, and discuss how to bypass hydration\u002FJavaScript crawling issues.",[15,13410,13412],{"id":13411},"_1-directives-configuring-robotstxt-for-ai-crawlers","1. Directives: Configuring Robots.txt for AI Crawlers",[11,13414,13415],{},"You should treat AI bots as distinct entities in your crawl configuration. While you want them to index your blog articles and service offerings so they can cite you, you must block them from indexing resources like API endpoints, user checkout routes, or local search configurations.",[11,13417,13418,13419,13421],{},"Here is a secure, production-ready ",[49,13420,13403],{}," configuration:",[1564,13423,13426],{"className":13424,"code":13425,"language":4815,"meta":363},[4813],"# Global rules for all standard crawlers\nUser-agent: *\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\nDisallow: \u002Fcheckout\u002F\n\n# OpenAI ChatGPT User Agent (Used to retrieve links for ChatGPT answers)\nUser-agent: GPTBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\nDisallow: \u002Fcheckout\u002F\nDisallow: \u002Fprivate\u002F\n\n# OpenAI Search Agent (Used for real-time search engine features)\nUser-agent: OAI-SearchBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\n\n# Anthropic Claude User Agent\nUser-agent: ClaudeBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\n\n# Perplexity Search Agent\nUser-agent: PerplexityBot\nAllow: \u002F\nDisallow: \u002Fadmin\u002F\nDisallow: \u002Fapi\u002F\n\n# Block aggressive, non-citing scrapers (Scrapers that steal data without giving traffic)\nUser-agent: CCBot\nDisallow: \u002F\nUser-agent: Amazonbot\nDisallow: \u002F\n",[49,13427,13425],{"__ignoreMap":363},[15,13429,13431],{"id":13430},"_2-implementing-nested-json-ld-schema-graphs","2. Implementing Nested JSON-LD Schema Graphs",[11,13433,13434],{},"LLMs do not parse text like human readers; they map concepts and values to semantic entities. If you describe your business in a plain paragraph, an LLM might misinterpret your prices, locations, or provider identities.",[11,13436,13437,13438,13441],{},"By injecting structured ",[26,13439,13440],{},"JSON-LD (JSON Linked Data)"," inside your page headers, you tell the crawlers exactly what your business represents.",[11,13443,13444,13445,13448,13449,8342],{},"Here is an advanced nested graph representing an ",[26,13446,13447],{},"Organization"," offering a specialized ",[26,13450,13451],{},"SaaS Development Service",[1564,13453,13455],{"className":1629,"code":13454,"language":1631,"meta":363,"style":363},"{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@graph\": [\n    {\n      \"@type\": \"Organization\",\n      \"@id\": \"https:\u002F\u002Falexrel.com\u002F#organization\",\n      \"name\": \"Alex Rel Development\",\n      \"url\": \"https:\u002F\u002Falexrel.com\",\n      \"logo\": \"https:\u002F\u002Falexrel.com\u002Flogo.png\",\n      \"sameAs\": [\n        \"https:\u002F\u002Fgithub.com\u002Falexrel\",\n        \"https:\u002F\u002Flinkedin.com\u002Fin\u002Falexrel\",\n        \"https:\u002F\u002Fx.com\u002F_alexrel\"\n      ]\n    },\n    {\n      \"@type\": \"Service\",\n      \"@id\": \"https:\u002F\u002Falexrel.com\u002Fsaas-development#service\",\n      \"name\": \"B2B SaaS Architecture and Development\",\n      \"description\": \"Full-cycle SaaS development from database schema isolation to Stripe webhook billing and vector support bots.\",\n      \"provider\": {\n        \"@id\": \"https:\u002F\u002Falexrel.com\u002F#organization\"\n      },\n      \"areaServed\": \"Worldwide\",\n      \"offers\": {\n        \"@type\": \"Offer\",\n        \"priceCurrency\": \"USD\",\n        \"price\": \"5000\",\n        \"priceSpecification\": {\n          \"@type\": \"UnitPriceSpecification\",\n          \"priceType\": \"https:\u002F\u002Fschema.org\u002FMinimumPrice\",\n          \"price\": \"5000\",\n          \"priceCurrency\": \"USD\"\n        }\n      }\n    }\n  ]\n}\n",[49,13456,13457,13461,13471,13479,13484,13495,13507,13519,13531,13543,13550,13557,13564,13569,13574,13579,13583,13594,13605,13616,13628,13635,13645,13650,13662,13669,13679,13690,13702,13709,13720,13732,13743,13753,13758,13762,13766,13770],{"__ignoreMap":363},[1572,13458,13459],{"class":1574,"line":1575},[1572,13460,1639],{"class":1638},[1572,13462,13463,13465,13467,13469],{"class":1574,"line":364},[1572,13464,13019],{"class":1644},[1572,13466,1648],{"class":1638},[1572,13468,13024],{"class":1651},[1572,13470,1655],{"class":1638},[1572,13472,13473,13476],{"class":1574,"line":370},[1572,13474,13475],{"class":1644},"  \"@graph\"",[1572,13477,13478],{"class":1638},": [\n",[1572,13480,13481],{"class":1574,"line":1591},[1572,13482,13483],{"class":1638},"    {\n",[1572,13485,13486,13488,13490,13493],{"class":1574,"line":1270},[1572,13487,13186],{"class":1644},[1572,13489,1648],{"class":1638},[1572,13491,13492],{"class":1651},"\"Organization\"",[1572,13494,1655],{"class":1638},[1572,13496,13497,13500,13502,13505],{"class":1574,"line":4389},[1572,13498,13499],{"class":1644},"      \"@id\"",[1572,13501,1648],{"class":1638},[1572,13503,13504],{"class":1651},"\"https:\u002F\u002Falexrel.com\u002F#organization\"",[1572,13506,1655],{"class":1638},[1572,13508,13509,13512,13514,13517],{"class":1574,"line":742},[1572,13510,13511],{"class":1644},"      \"name\"",[1572,13513,1648],{"class":1638},[1572,13515,13516],{"class":1651},"\"Alex Rel Development\"",[1572,13518,1655],{"class":1638},[1572,13520,13521,13524,13526,13529],{"class":1574,"line":10336},[1572,13522,13523],{"class":1644},"      \"url\"",[1572,13525,1648],{"class":1638},[1572,13527,13528],{"class":1651},"\"https:\u002F\u002Falexrel.com\"",[1572,13530,1655],{"class":1638},[1572,13532,13533,13536,13538,13541],{"class":1574,"line":10342},[1572,13534,13535],{"class":1644},"      \"logo\"",[1572,13537,1648],{"class":1638},[1572,13539,13540],{"class":1651},"\"https:\u002F\u002Falexrel.com\u002Flogo.png\"",[1572,13542,1655],{"class":1638},[1572,13544,13545,13548],{"class":1574,"line":10348},[1572,13546,13547],{"class":1644},"      \"sameAs\"",[1572,13549,13478],{"class":1638},[1572,13551,13552,13555],{"class":1574,"line":11656},[1572,13553,13554],{"class":1651},"        \"https:\u002F\u002Fgithub.com\u002Falexrel\"",[1572,13556,1655],{"class":1638},[1572,13558,13559,13562],{"class":1574,"line":11661},[1572,13560,13561],{"class":1651},"        \"https:\u002F\u002Flinkedin.com\u002Fin\u002Falexrel\"",[1572,13563,1655],{"class":1638},[1572,13565,13566],{"class":1574,"line":11671},[1572,13567,13568],{"class":1651},"        \"https:\u002F\u002Fx.com\u002F_alexrel\"\n",[1572,13570,13571],{"class":1574,"line":11683},[1572,13572,13573],{"class":1638},"      ]\n",[1572,13575,13576],{"class":1574,"line":11695},[1572,13577,13578],{"class":1638},"    },\n",[1572,13580,13581],{"class":1574,"line":11700},[1572,13582,13483],{"class":1638},[1572,13584,13585,13587,13589,13592],{"class":1574,"line":11705},[1572,13586,13186],{"class":1644},[1572,13588,1648],{"class":1638},[1572,13590,13591],{"class":1651},"\"Service\"",[1572,13593,1655],{"class":1638},[1572,13595,13596,13598,13600,13603],{"class":1574,"line":11742},[1572,13597,13499],{"class":1644},[1572,13599,1648],{"class":1638},[1572,13601,13602],{"class":1651},"\"https:\u002F\u002Falexrel.com\u002Fsaas-development#service\"",[1572,13604,1655],{"class":1638},[1572,13606,13607,13609,13611,13614],{"class":1574,"line":11763},[1572,13608,13511],{"class":1644},[1572,13610,1648],{"class":1638},[1572,13612,13613],{"class":1651},"\"B2B SaaS Architecture and Development\"",[1572,13615,1655],{"class":1638},[1572,13617,13618,13621,13623,13626],{"class":1574,"line":11787},[1572,13619,13620],{"class":1644},"      \"description\"",[1572,13622,1648],{"class":1638},[1572,13624,13625],{"class":1651},"\"Full-cycle SaaS development from database schema isolation to Stripe webhook billing and vector support bots.\"",[1572,13627,1655],{"class":1638},[1572,13629,13630,13633],{"class":1574,"line":11793},[1572,13631,13632],{"class":1644},"      \"provider\"",[1572,13634,13081],{"class":1638},[1572,13636,13637,13640,13642],{"class":1574,"line":11806},[1572,13638,13639],{"class":1644},"        \"@id\"",[1572,13641,1648],{"class":1638},[1572,13643,13644],{"class":1651},"\"https:\u002F\u002Falexrel.com\u002F#organization\"\n",[1572,13646,13647],{"class":1574,"line":11820},[1572,13648,13649],{"class":1638},"      },\n",[1572,13651,13652,13655,13657,13660],{"class":1574,"line":11826},[1572,13653,13654],{"class":1644},"      \"areaServed\"",[1572,13656,1648],{"class":1638},[1572,13658,13659],{"class":1651},"\"Worldwide\"",[1572,13661,1655],{"class":1638},[1572,13663,13664,13667],{"class":1574,"line":11831},[1572,13665,13666],{"class":1644},"      \"offers\"",[1572,13668,13081],{"class":1638},[1572,13670,13671,13673,13675,13677],{"class":1574,"line":11838},[1572,13672,13228],{"class":1644},[1572,13674,1648],{"class":1638},[1572,13676,13148],{"class":1651},[1572,13678,1655],{"class":1638},[1572,13680,13681,13684,13686,13688],{"class":1574,"line":11890},[1572,13682,13683],{"class":1644},"        \"priceCurrency\"",[1572,13685,1648],{"class":1638},[1572,13687,13160],{"class":1651},[1572,13689,1655],{"class":1638},[1572,13691,13692,13695,13697,13700],{"class":1574,"line":11895},[1572,13693,13694],{"class":1644},"        \"price\"",[1572,13696,1648],{"class":1638},[1572,13698,13699],{"class":1651},"\"5000\"",[1572,13701,1655],{"class":1638},[1572,13703,13704,13707],{"class":1574,"line":11901},[1572,13705,13706],{"class":1644},"        \"priceSpecification\"",[1572,13708,13081],{"class":1638},[1572,13710,13711,13714,13716,13718],{"class":1574,"line":11914},[1572,13712,13713],{"class":1644},"          \"@type\"",[1572,13715,1648],{"class":1638},[1572,13717,13191],{"class":1651},[1572,13719,1655],{"class":1638},[1572,13721,13722,13725,13727,13730],{"class":1574,"line":11956},[1572,13723,13724],{"class":1644},"          \"priceType\"",[1572,13726,1648],{"class":1638},[1572,13728,13729],{"class":1651},"\"https:\u002F\u002Fschema.org\u002FMinimumPrice\"",[1572,13731,1655],{"class":1638},[1572,13733,13734,13737,13739,13741],{"class":1574,"line":11976},[1572,13735,13736],{"class":1644},"          \"price\"",[1572,13738,1648],{"class":1638},[1572,13740,13699],{"class":1651},[1572,13742,1655],{"class":1638},[1572,13744,13745,13748,13750],{"class":1574,"line":11981},[1572,13746,13747],{"class":1644},"          \"priceCurrency\"",[1572,13749,1648],{"class":1638},[1572,13751,13752],{"class":1651},"\"USD\"\n",[1572,13754,13755],{"class":1574,"line":11987},[1572,13756,13757],{"class":1638},"        }\n",[1572,13759,13760],{"class":1574,"line":11993},[1572,13761,13261],{"class":1638},[1572,13763,13764],{"class":1574,"line":12006},[1572,13765,12625],{"class":1638},[1572,13767,13768],{"class":1574,"line":12026},[1572,13769,12352],{"class":1638},[1572,13771,13772],{"class":1574,"line":12037},[1572,13773,1670],{"class":1638},[15,13775,13777],{"id":13776},"_3-resolving-client-side-rendering-csr-issues-for-ai-bots","3. Resolving Client-Side Rendering (CSR) Issues for AI Bots",[11,13779,13780],{},"Unlike Googlebot, which allocates massive CPU resources to render JavaScript and execute client-side hydration, many AI search scraper agents are lightweight tools that pull raw HTML responses to minimize processing time.",[11,13782,13783,13784,13787],{},"If your web application relies entirely on client-side rendering (e.g., a standard single-page React or Vue app with an empty ",[49,13785,13786],{},"\u003Cdiv id=\"app\">\u003C\u002Fdiv>","), AI crawlers might only see a blank page, excluding your brand from their index.",[70,13789,13791],{"id":13790},"how-to-solve-this-in-nuxt-3","How to solve this in Nuxt 3:",[4001,13793,13794,13807],{},[301,13795,13796,13799,13800,13803,13804,507],{},[26,13797,13798],{},"Enable Server-Side Rendering (SSR)",": Ensure ",[49,13801,13802],{},"ssr: true"," is configured in your ",[49,13805,13806],{},"nuxt.config.ts",[301,13808,13809,13812,13813,1121,13815,13818],{},[26,13810,13811],{},"Use Pre-rendering for Static Pages",": Configure routes like ",[49,13814,12750],{},[49,13816,13817],{},"\u002Fsaas-development"," to build as static HTML files during deployment:",[1564,13820,13822],{"className":11542,"code":13821,"language":11544,"meta":363,"style":363},"\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  routeRules: {\n    '\u002F': { prerender: true },\n    '\u002Fsaas-development': { prerender: true },\n    '\u002Ftelegram-bots': { prerender: true },\n    '\u002Fgeo-seo': { prerender: true },\n    '\u002Fconsultations': { prerender: true }\n  }\n});\n",[49,13823,13824,13829,13841,13846,13859,13870,13881,13892,13903,13907],{"__ignoreMap":363},[1572,13825,13826],{"class":1574,"line":1575},[1572,13827,13828],{"class":11834},"\u002F\u002F nuxt.config.ts\n",[1572,13830,13831,13833,13836,13839],{"class":1574,"line":364},[1572,13832,3790],{"class":11551},[1572,13834,13835],{"class":11551}," default",[1572,13837,13838],{"class":6311}," defineNuxtConfig",[1572,13840,12326],{"class":1638},[1572,13842,13843],{"class":1574,"line":370},[1572,13844,13845],{"class":1638},"  routeRules: {\n",[1572,13847,13848,13851,13854,13856],{"class":1574,"line":1591},[1572,13849,13850],{"class":1651},"    '\u002F'",[1572,13852,13853],{"class":1638},": { prerender: ",[1572,13855,3638],{"class":1644},[1572,13857,13858],{"class":1638}," },\n",[1572,13860,13861,13864,13866,13868],{"class":1574,"line":1270},[1572,13862,13863],{"class":1651},"    '\u002Fsaas-development'",[1572,13865,13853],{"class":1638},[1572,13867,3638],{"class":1644},[1572,13869,13858],{"class":1638},[1572,13871,13872,13875,13877,13879],{"class":1574,"line":4389},[1572,13873,13874],{"class":1651},"    '\u002Ftelegram-bots'",[1572,13876,13853],{"class":1638},[1572,13878,3638],{"class":1644},[1572,13880,13858],{"class":1638},[1572,13882,13883,13886,13888,13890],{"class":1574,"line":742},[1572,13884,13885],{"class":1651},"    '\u002Fgeo-seo'",[1572,13887,13853],{"class":1638},[1572,13889,3638],{"class":1644},[1572,13891,13858],{"class":1638},[1572,13893,13894,13897,13899,13901],{"class":1574,"line":10336},[1572,13895,13896],{"class":1651},"    '\u002Fconsultations'",[1572,13898,13853],{"class":1638},[1572,13900,3638],{"class":1644},[1572,13902,12347],{"class":1638},[1572,13904,13905],{"class":1574,"line":10342},[1572,13906,11823],{"class":1638},[1572,13908,13909],{"class":1574,"line":10348},[1572,13910,13911],{"class":1638},"});\n",[11,13913,13914],{},"Enforcing static rendering ensures that whenever GPTBot or ClaudeBot fetches your pages, they receive full, semantic text structures immediately.",[11,13916,13917,13918,1121,13921,13923],{},"When establishing the indexing architecture for ",[38,13919,13920],{"href":13282},"LingvoHabit",[38,13922,12706],{"href":12705},", we deployed these exact pre-rendered configurations and nested schemas. This resulted in immediate citations across major AI search indexes.",[15,13925,12723],{"id":12722},[298,13927,13928,13934,13942,13950],{},[301,13929,13930,13933],{},[38,13931,13318],{"href":13316,"rel":13932},[429]," — current GPTBot and OAI-SearchBot user agents",[301,13935,13936,13941],{},[38,13937,13940],{"href":13938,"rel":13939},"https:\u002F\u002Fsupport.anthropic.com\u002Fen\u002Farticles\u002F8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler",[429],"Anthropic: how ClaudeBot works"," — official Claude crawler rules",[301,13943,13944,13949],{},[38,13945,13948],{"href":13946,"rel":13947},"https:\u002F\u002Fschema.org",[429],"Schema.org"," — the full structured-data vocabulary",[301,13951,13952,13957],{},[38,13953,13956],{"href":13954,"rel":13955},"https:\u002F\u002Fdevelopers.google.com\u002Fsearch\u002Fdocs\u002Fcrawling-indexing\u002Frobots\u002Fintro",[429],"Google: intro to robots.txt"," — directive syntax and precedence",[11,13959,13960,13961,507],{},"Structuring your technical SEO layer for AI scraping ensures that your site functions as an open, readable directory for the LLM agents dominating the next generation of web search. To learn how this technical layout integrates with a full AI-first marketing strategy, check out my introduction to ",[38,13962,12812],{"href":13963},"\u002Fblog\u002Fgenerative-engine-optimization-geo",[11,13965,13966,13967,13970,13971,507],{},"If you need a professional developer to implement structured JSON-LD schemas, configure SSR routes, or audit your robots.txt setup, visit my ",[38,13968,13969],{"href":13328},"GEO & SEO Services"," page or book a ",[38,13972,13333],{"href":12755},[1779,13974,13975],{},"html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":363,"searchDepth":364,"depth":364,"links":13977},[13978,13979,13980,13983],{"id":13411,"depth":364,"text":13412},{"id":13430,"depth":364,"text":13431},{"id":13776,"depth":364,"text":13777,"children":13981},[13982],{"id":13790,"depth":370,"text":13791},{"id":12722,"depth":364,"text":12723},"2026-06-28","A technical masterclass on configuring robots.txt controls for AI agents, implementing nested JSON-LD schema graphs, and solving client-side rendering hurdles.",[13987,13990,13993],{"q":13988,"a":13989},"Should I allow every AI bot to crawl my site?","No. Separate bots by purpose: search and answer bots (GPTBot, OAI-SearchBot, ClaudeBot, PerplexityBot) are worth allowing — they bring citations and traffic. Pure training crawlers like CCBot can be blocked: they collect data without sending anything back.",{"q":13991,"a":13992},"Will AI crawlers see the content of my Vue or React SPA?","Not reliably. Some AI bots don't execute JavaScript, so a client-rendered app looks like an empty page to them. The fix is SSR or prerendering: the server must return complete HTML with text and markup.",{"q":13994,"a":13995},"Which JSON-LD markup matters most for AI search?","A linked @graph with Organization\u002FPerson (who you are), Service or Product (what you offer), and WebPage with dateModified. It's critical that the markup matches the visible content exactly — divergence undermines trust in the source.","\u002Fimages\u002Fblog\u002Fblog_ai_crawlers.webp",{},"\u002Fblog\u002Fen\u002Fwebsite-optimization-ai-crawlers",{"title":13374,"description":13985},"blog\u002Fen\u002Fwebsite-optimization-ai-crawlers",[13367,13368,13407,14002,14003],"Schema","Technical","v20o3h8eEEpy9LccMwXLQ673589bb2UI_I7MqTL0iSA",{"id":14006,"title":14007,"body":14008,"date":14230,"description":14231,"extension":376,"faq":14232,"image":14245,"lang":394,"meta":14246,"navigation":396,"path":14247,"published":396,"readTime":370,"seo":14248,"stem":14249,"tags":14250,"updated":12772,"__hash__":14254},"blog\u002Fblog\u002Fen\u002Fsaas-mvp-development-cost.md","How Much Does SaaS MVP Development Cost in 2026: A Stage-by-Stage Estimate",{"type":8,"value":14009,"toc":14224},[14010,14013,14017,14020,14089,14096,14100,14103,14136,14140,14143,14174,14195,14199,14206,14213],[11,14011,14012],{},"Turnkey SaaS MVP development costs from $4,000 and takes 4–6 weeks — that buys a working product with registration, user dashboards, subscription billing, and an admin panel. A full platform with integrations costs more and takes 2–4 months. Below: which stages make up the estimate, where post-launch costs hide, and why a \"$1,000 MVP\" usually turns out to be the most expensive option.",[15,14014,14016],{"id":14015},"which-stages-make-up-the-price","Which stages make up the price?",[11,14018,14019],{},"My SaaS development process has four stages, and each is a separate line in the estimate:",[75,14021,14022,14035],{},[78,14023,14024],{},[81,14025,14026,14029,14032],{},[84,14027,14028],{"align":12854},"Stage",[84,14030,14031],{"align":12854},"What happens",[84,14033,14034],{"align":12854},"Budget share",[91,14036,14037,14050,14063,14076],{},[81,14038,14039,14044,14047],{},[96,14040,14041],{"align":12854},[26,14042,14043],{},"1. Analysis & spec",[96,14045,14046],{"align":12854},"Architecture design, database schema, stack selection, detailed specification",[96,14048,14049],{"align":12854},"~10–15%",[81,14051,14052,14057,14060],{},[96,14053,14054],{"align":12854},[26,14055,14056],{},"2. MVP",[96,14058,14059],{"align":12854},"Registration, user dashboards, the product core, Stripe billing",[96,14061,14062],{"align":12854},"~50%",[81,14064,14065,14070,14073],{},[96,14066,14067],{"align":12854},[26,14068,14069],{},"3. Scaling",[96,14071,14072],{"align":12854},"External API integrations, team accounts, pricing plans",[96,14074,14075],{"align":12854},"~25%",[81,14077,14078,14083,14086],{},[96,14079,14080],{"align":12854},[26,14081,14082],{},"4. Launch",[96,14084,14085],{"align":12854},"Server deployment, backups, monitoring, final testing",[96,14087,14088],{"align":12854},"~10%",[11,14090,14091,14092,14095],{},"The key point: half the budget goes into the ",[26,14093,14094],{},"product core"," — the feature customers will pay for. If a contractor spends half the estimate on \"a beautiful landing page and animations,\" the product has a priorities problem.",[15,14097,14099],{"id":14098},"why-not-just-use-a-no-code-builder","Why not just use a no-code builder?",[11,14101,14102],{},"Sometimes you should: for validating demand without unique logic, no-code honestly saves months. Custom development becomes justified when at least one of these appears:",[298,14104,14105,14111,14120,14130],{},[301,14106,14107,14110],{},[26,14108,14109],{},"unique business logic"," — algorithms, calculations, automations that don't exist as ready-made blocks;",[301,14112,14113,14116,14117,8971],{},[26,14114,14115],{},"subscriptions and complex billing"," — upgrades, grace periods, payment webhooks (I covered the technical side in ",[38,14118,14119],{"href":12717},"my Stripe integration guide",[301,14121,14122,14125,14126,8971],{},[26,14123,14124],{},"data requirements"," — tenant isolation, exports, regulatory compliance (see my breakdown of ",[38,14127,14129],{"href":14128},"\u002Fblog\u002Fsaas-multitenancy-databases","multi-tenant architectures",[301,14131,14132,14135],{},[26,14133,14134],{},"a scaling plan"," — builders hit a ceiling, and migrating off them costs more than building custom from the start.",[15,14137,14139],{"id":14138},"where-do-post-launch-costs-hide","Where do post-launch costs hide?",[11,14141,14142],{},"The development estimate is not the full cost of ownership. Budget for:",[298,14144,14145,14151,14162,14168],{},[301,14146,14147,14150],{},[26,14148,14149],{},"Infrastructure"," — a VPS or cloud, from tens of dollars a month at the start, growing with load.",[301,14152,14153,14156,14157,8399],{},[26,14154,14155],{},"Payment fees"," — Stripe takes a percentage of every transaction (",[38,14158,14161],{"href":14159,"rel":14160},"https:\u002F\u002Fstripe.com\u002Fpricing",[429],"Stripe pricing",[301,14163,14164,14167],{},[26,14165,14166],{},"External APIs"," — email services, AI models, analytics: billed by usage.",[301,14169,14170,14173],{},[26,14171,14172],{},"Iteration"," — a SaaS without updates dies; reserve budget for changes after the first user feedback.",[11,14175,14176,14177,4110,14180,14185,14186,4110,14189,14194],{},"Meanwhile the market keeps growing: global SaaS spending is forecast at ",[26,14178,14179],{},"$300B",[38,14181,14184],{"href":14182,"rel":14183},"https:\u002F\u002Fwww.gartner.com\u002Fen\u002Fnewsroom\u002Fpress-releases\u002F2024-08-28-gartner-predicts-75-percent-of-enterprises-will-prioritize-backup-of-saas-applications-as-a-critical-requirement-by-2028",[429],"Gartner, 2024","), and public SaaS companies trade at a median ",[26,14187,14188],{},"7.0x annual revenue",[38,14190,14193],{"href":14191,"rel":14192},"https:\u002F\u002Fwww.saas-capital.com\u002Fthe-saas-capital-index\u002F",[429],"SaaS Capital Index",") — a well-built product still has some of the best payback economics in the industry.",[15,14196,14198],{"id":14197},"why-is-the-cheap-mvp-the-most-expensive-one","Why is the \"cheap MVP\" the most expensive one?",[11,14200,14201,14202,507],{},"The main budgeting risk is not overpaying — it's paying twice. A $1,000 MVP from a bargain agency almost always means junior developers without code review, no tests, and template code that cannot scale. Within 3–6 months such a project gets rewritten from scratch — I broke down this trap with the math in ",[38,14203,14205],{"href":14204},"\u002Fblog\u002Fsaas-development-cheap-code-trap","The price of cheap code",[11,14207,14208,14209,507],{},"What the right alternative looks like, step by step with architecture diagrams, is in my guide ",[38,14210,14212],{"href":14211},"\u002Fblog\u002Fb2b-saas-mvp-30-days","How to build a B2B SaaS MVP in 30 days",[11,14214,14215,14216,14219,14220,14223],{},"Want an estimate for your idea? Describe the product on the ",[38,14217,14218],{"href":13817},"custom SaaS development page"," — I reply within 24 hours with a budget and timeline. If the idea is still raw, start with a ",[38,14221,14222],{"href":12755},"technical consultation",": in one hour we'll work through the architecture, the stack, and a realistic launch plan.",{"title":363,"searchDepth":364,"depth":364,"links":14225},[14226,14227,14228,14229],{"id":14015,"depth":364,"text":14016},{"id":14098,"depth":364,"text":14099},{"id":14138,"depth":364,"text":14139},{"id":14197,"depth":364,"text":14198},"2026-06-25","What a SaaS MVP price is made of — from $4,000 for a working product with billing. Stages, timelines, stack, and post-launch costs without marketing fluff.",[14233,14236,14239,14242],{"q":14234,"a":14235},"How long does SaaS MVP development take?","A working MVP with user dashboards and billing takes 4–6 weeks. A full platform with integrations, team accounts, and an admin panel takes 2–4 months. Timelines count from an approved specification.",{"q":14237,"a":14238},"What is included in the from-$4,000 price?","Architecture design and specification, MVP development with registration and user dashboards, subscription payments, an admin panel, server deployment, and monitoring setup.",{"q":14240,"a":14241},"Which stack do you use for SaaS projects?","Vue 3 \u002F Nuxt 3 on the frontend; Node.js, Python (FastAPI), or Laravel on the backend; PostgreSQL and Redis for data; Docker for deployment. The stack is chosen to fit the task, not the other way around.",{"q":14243,"a":14244},"What happens to the project after launch?","After launch I either take the project into maintenance (bug fixes, monitoring, feature development) or hand it over to your team with documentation: architecture, API, and deployment instructions.","\u002Fimages\u002Fblog\u002Fblog_saas_mvp_cost.webp",{},"\u002Fblog\u002Fen\u002Fsaas-mvp-development-cost",{"title":14007,"description":14231},"blog\u002Fen\u002Fsaas-mvp-development-cost",[12792,14251,14252,14253],"MVP","Pricing","Startups","DYzE8oRIXsLiat3zEsxVbFTyNicF-NDafd6_bsrU94o",{"id":14256,"title":14257,"body":14258,"date":14900,"description":14901,"extension":376,"faq":14902,"image":14912,"lang":394,"meta":14913,"navigation":396,"path":14914,"published":396,"readTime":1591,"seo":14915,"stem":14916,"tags":14917,"updated":12772,"__hash__":14920},"blog\u002Fblog\u002Fen\u002Fb2b-saas-mvp-30-days.md","How to Build a B2B SaaS MVP in 30 Days Without Technical Debt",{"type":8,"value":14259,"toc":14882},[14260,14263,14270,14274,14277,14280,14286,14290,14293,14300,14304,14307,14331,14335,14346,14366,14370,14373,14377,14380,14384,14395,14405,14408,14412,14415,14419,14422,14426,14440,14446,14450,14453,14808,14812,14844,14846,14871,14879],[11,14261,14262],{},"Building a Minimum Viable Product (MVP) for a B2B SaaS is a delicate balancing act. Launch too slowly, and you risk missing the market window, running out of capital, or building features nobody wants. Build too quickly, and you might bury your project in a mountain of technical debt that makes future scaling, refactoring, and feature additions practically impossible.",[11,14264,14265,14266,14269],{},"In my years of developing custom SaaS products (like TeleGo.io and LingvoHabit), I’ve refined a ",[26,14267,14268],{},"30-day MVP blueprint",". It focuses on maximizing developer velocity and optimizing for AI crawlers (GEO) and search engines (SEO) without sacrificing clean code architecture. Here is a deep dive into building a production-ready, scalable B2B SaaS MVP in 30 days.",[15,14271,14273],{"id":14272},"the-core-technical-challenge-of-saas-mvps","The Core Technical Challenge of SaaS MVPs",[11,14275,14276],{},"Most B2B SaaS failures are not due to scaling issues like handling millions of concurrent requests. Instead, they fail because the codebase becomes a tangled \"spaghetti\" of dependencies where fixing one bug introduces three new ones.",[11,14278,14279],{},"To prevent this, you must choose a tech stack that enforces separation of concerns out of the box while allowing rapid prototyping.",[1564,14281,14284],{"className":14282,"code":14283,"language":4815,"meta":363},[4813],"+--------------------------------------------------------+\n|                      Nuxt 3 App                        |\n|   (Universal SSR Frontend + Optimized SEO Head Meta)  |\n+---------------------------+----------------------------+\n                            | (HTTP\u002FJSON API)\n                            v\n+--------------------------------------------------------+\n|                Nitro Engine API Routes                 |\n|   (Secure Authentication -> Domain Services Logic)    |\n+---------------------------+----------------------------+\n                            | (Database Query)\n                            v\n+--------------------------------------------------------+\n|           SQLite Database (better-sqlite3)             |\n|  (Indexed tables, transaction safety, fast raw I\u002FO)  |\n+--------------------------------------------------------+\n",[49,14285,14283],{"__ignoreMap":363},[15,14287,14289],{"id":14288},"_1-the-lean-tech-stack-optimized-for-speed-and-seo","1. The Lean Tech Stack (Optimized for Speed and SEO)",[11,14291,14292],{},"For a 30-day launch, your stack must offer high developer velocity, out-of-the-box Server-Side Rendering (SSR), and a highly reliable database layer.",[11,14294,14295,14296,14299],{},"For example, when building the ",[38,14297,14298],{"href":13282},"LingvoHabit MVP",", a multi-tenant language-learning platform, we utilized this exact frontend structure to ensure swift loading times and excellent search engine indexing.",[70,14301,14303],{"id":14302},"frontend-ssr-nuxt-3-vue-3-vite","Frontend & SSR: Nuxt 3 (Vue 3, Vite)",[11,14305,14306],{},"Nuxt 3 is the ultimate framework for modern SaaS development. It provides:",[298,14308,14309,14315,14321],{},[301,14310,14311,14314],{},[26,14312,14313],{},"Built-in Server-Side Rendering (SSR)",": Critical for traditional search engines (Google\u002FYandex) and AI search bots (Perplexity\u002FChatGPT) which need to scan static HTML to index and cite your brand.",[301,14316,14317,14320],{},[26,14318,14319],{},"Automatic Routing",": Simplifies page management.",[301,14322,14323,14326,14327,14330],{},[26,14324,14325],{},"Nitro Engine",": A fast server engine that lets you write backend API endpoints directly in ",[49,14328,14329],{},"\u002Fserver\u002Fapi"," using TypeScript.",[70,14332,14334],{"id":14333},"database-sqlite-via-better-sqlite3","Database: SQLite via better-sqlite3",[11,14336,14337,14338,14341,14342,14345],{},"Do not waste time setting up complex PostgreSQL clusters, AWS RDS instances, or Kubernetes pods in week one. Use ",[26,14339,14340],{},"SQLite"," via the ",[49,14343,14344],{},"better-sqlite3"," library.",[298,14347,14348,14354,14360],{},[301,14349,14350,14353],{},[26,14351,14352],{},"Performance",": SQLite runs in-memory or as a local file, performing transactions in microseconds. It easily handles 50-100 concurrent writes and thousands of reads per second, which is more than enough for an MVP.",[301,14355,14356,14359],{},[26,14357,14358],{},"Portability",": Backing up your database is as simple as copying a single file.",[301,14361,14362,14365],{},[26,14363,14364],{},"Scaling path",": When you hit limits, you can migrate to PostgreSQL in under an hour if you use an ORM or write standard SQL queries.",[15,14367,14369],{"id":14368},"_2-the-core-features-checklist","2. The Core Features Checklist",[11,14371,14372],{},"A B2B SaaS MVP only needs four functional modules to start onboarding and billing clients:",[70,14374,14376],{"id":14375},"a-authentication-session-based-or-passwordless","A. Authentication (Session-based or Passwordless)",[11,14378,14379],{},"Skip complex OAuth flows or custom identity providers. Implement passwordless magic links or standard email\u002Fpassword authentication using HTTP-only cookies to store JWT tokens securely.",[70,14381,14383],{"id":14382},"b-subscription-billing-stripe-integration","B. Subscription Billing (Stripe Integration)",[11,14385,14386,14387,14390,14391,14394],{},"Integrate ",[26,14388,14389],{},"Stripe Checkout"," for payment processing and ",[26,14392,14393],{},"Stripe Customer Portal"," to let users manage their subscriptions.",[11,14396,14397,14398,14401,14402,507],{},"For instance, in the ",[38,14399,14400],{"href":12705},"TeleGo.io project",", we integrated this Stripe flow to handle hundreds of active subscriptions. For a deep technical dive on this, read my guide on ",[38,14403,14404],{"href":12717},"Stripe Billing and Subscriptions Integration",[11,14406,14407],{},"This offloads the UI for card updates, billing history, and cancellations directly to Stripe, saving you at least 5 days of frontend development.",[70,14409,14411],{"id":14410},"c-the-core-value-loop","C. The Core Value Loop",[11,14413,14414],{},"This is the unique feature your clients are paying for (e.g., automated report generation, CRM data syncing, or AI text editing). Dedicate 50% of your time here.",[70,14416,14418],{"id":14417},"d-admin-telemetry-dashboard","D. Admin \u002F Telemetry Dashboard",[11,14420,14421],{},"A minimal interface for you (the founder) to track user signups, active subscriptions, and raw usage metrics.",[15,14423,14425],{"id":14424},"_3-designing-a-scalable-code-architecture-no-spaghetti","3. Designing a Scalable Code Architecture (No Spaghetti)",[11,14427,14428,14429,14432,14433,14436,14437,507],{},"To prevent a rewrite in month three, structure your server-side logic by ",[26,14430,14431],{},"business domains"," (modules) rather than technical layers. For example, group all user-related code inside a ",[49,14434,14435],{},"users"," domain. If you want to learn how to isolate data per client, check my article on ",[38,14438,14439],{"href":14128},"SaaS Multi-Tenant Database Architectures",[1564,14441,14444],{"className":14442,"code":14443,"language":4815,"meta":363},[4813],"server\u002F\n├── api\u002F\n│   ├── auth\u002F\n│   ├── billing\u002F\n│   └── projects\u002F\n├── database\u002F\n│   ├── connection.ts\n│   └── schema.sql\n└── services\u002F\n    ├── auth.service.ts\n    ├── stripe.service.ts\n    └── project.service.ts\n",[49,14445,14443],{"__ignoreMap":363},[70,14447,14449],{"id":14448},"implementing-code-isolation-stripe-service-example","Implementing Code Isolation: Stripe Service Example",[11,14451,14452],{},"Keep your business logic isolated from HTTP request handlers. Here is a professional TypeScript implementation of a Stripe subscription service:",[1564,14454,14456],{"className":11542,"code":14455,"language":11544,"meta":363,"style":363},"\u002F\u002F server\u002Fservices\u002Fstripe.service.ts\nimport Stripe from 'stripe';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: '2023-10-16',\n});\n\nexport interface SubscriptionInput {\n  email: string;\n  priceId: string;\n  tenantId: string;\n}\n\nexport async function createCheckoutSession(input: SubscriptionInput): Promise\u003Cstring> {\n  const session = await stripe.checkout.sessions.create({\n    payment_method_types: ['card'],\n    line_items: [\n      {\n        price: input.priceId,\n        quantity: 1,\n      },\n    ],\n    mode: 'subscription',\n    success_url: `${process.env.APP_URL}\u002Fdashboard?billing=success`,\n    cancel_url: `${process.env.APP_URL}\u002Fdashboard?billing=cancel`,\n    customer_email: input.email,\n    metadata: {\n      tenantId: input.tenantId,\n    },\n  });\n\n  if (!session.url) {\n    throw new Error('Failed to generate Stripe checkout URL.');\n  }\n\n  return session.url;\n}\n",[49,14457,14458,14463,14477,14481,14506,14516,14520,14524,14536,14547,14558,14569,14573,14577,14612,14632,14643,14648,14653,14658,14667,14671,14676,14686,14712,14734,14739,14744,14749,14753,14757,14761,14772,14789,14793,14797,14804],{"__ignoreMap":363},[1572,14459,14460],{"class":1574,"line":1575},[1572,14461,14462],{"class":11834},"\u002F\u002F server\u002Fservices\u002Fstripe.service.ts\n",[1572,14464,14465,14467,14470,14472,14475],{"class":1574,"line":364},[1572,14466,5892],{"class":11551},[1572,14468,14469],{"class":1638}," Stripe ",[1572,14471,11557],{"class":11551},[1572,14473,14474],{"class":1651}," 'stripe'",[1572,14476,11563],{"class":1638},[1572,14478,14479],{"class":1574,"line":370},[1572,14480,10309],{"emptyLinePlaceholder":396},[1572,14482,14483,14485,14488,14490,14492,14495,14498,14501,14503],{"class":1574,"line":1591},[1572,14484,5852],{"class":11551},[1572,14486,14487],{"class":1644}," stripe",[1572,14489,11751],{"class":11551},[1572,14491,11754],{"class":11551},[1572,14493,14494],{"class":6311}," Stripe",[1572,14496,14497],{"class":1638},"(process.env.",[1572,14499,14500],{"class":1644},"STRIPE_SECRET_KEY",[1572,14502,5583],{"class":11551},[1572,14504,14505],{"class":1638},", {\n",[1572,14507,14508,14511,14514],{"class":1574,"line":1270},[1572,14509,14510],{"class":1638},"  apiVersion: ",[1572,14512,14513],{"class":1651},"'2023-10-16'",[1572,14515,1655],{"class":1638},[1572,14517,14518],{"class":1574,"line":4389},[1572,14519,13911],{"class":1638},[1572,14521,14522],{"class":1574,"line":742},[1572,14523,10309],{"emptyLinePlaceholder":396},[1572,14525,14526,14528,14531,14534],{"class":1574,"line":10336},[1572,14527,3790],{"class":11551},[1572,14529,14530],{"class":11551}," interface",[1572,14532,14533],{"class":6311}," SubscriptionInput",[1572,14535,11578],{"class":1638},[1572,14537,14538,14541,14543,14545],{"class":1574,"line":10342},[1572,14539,14540],{"class":11583},"  email",[1572,14542,8342],{"class":11551},[1572,14544,11601],{"class":1644},[1572,14546,11563],{"class":1638},[1572,14548,14549,14552,14554,14556],{"class":1574,"line":10348},[1572,14550,14551],{"class":11583},"  priceId",[1572,14553,8342],{"class":11551},[1572,14555,11601],{"class":1644},[1572,14557,11563],{"class":1638},[1572,14559,14560,14563,14565,14567],{"class":1574,"line":11656},[1572,14561,14562],{"class":11583},"  tenantId",[1572,14564,8342],{"class":11551},[1572,14566,11601],{"class":1644},[1572,14568,11563],{"class":1638},[1572,14570,14571],{"class":1574,"line":11661},[1572,14572,1670],{"class":1638},[1572,14574,14575],{"class":1574,"line":11671},[1572,14576,10309],{"emptyLinePlaceholder":396},[1572,14578,14579,14581,14584,14586,14589,14591,14593,14595,14597,14599,14601,14604,14606,14609],{"class":1574,"line":11683},[1572,14580,3790],{"class":11551},[1572,14582,14583],{"class":11551}," async",[1572,14585,7934],{"class":11551},[1572,14587,14588],{"class":6311}," createCheckoutSession",[1572,14590,11715],{"class":1638},[1572,14592,6239],{"class":11583},[1572,14594,8342],{"class":11551},[1572,14596,14533],{"class":6311},[1572,14598,219],{"class":1638},[1572,14600,8342],{"class":11551},[1572,14602,14603],{"class":6311}," Promise",[1572,14605,6305],{"class":1638},[1572,14607,14608],{"class":1644},"string",[1572,14610,14611],{"class":1638},"> {\n",[1572,14613,14614,14616,14619,14621,14624,14627,14630],{"class":1574,"line":11695},[1572,14615,11745],{"class":11551},[1572,14617,14618],{"class":1644}," session",[1572,14620,11751],{"class":11551},[1572,14622,14623],{"class":11551}," await",[1572,14625,14626],{"class":1638}," stripe.checkout.sessions.",[1572,14628,14629],{"class":6311},"create",[1572,14631,12326],{"class":1638},[1572,14633,14634,14637,14640],{"class":1574,"line":11700},[1572,14635,14636],{"class":1638},"    payment_method_types: [",[1572,14638,14639],{"class":1651},"'card'",[1572,14641,14642],{"class":1638},"],\n",[1572,14644,14645],{"class":1574,"line":11705},[1572,14646,14647],{"class":1638},"    line_items: [\n",[1572,14649,14650],{"class":1574,"line":11742},[1572,14651,14652],{"class":1638},"      {\n",[1572,14654,14655],{"class":1574,"line":11763},[1572,14656,14657],{"class":1638},"        price: input.priceId,\n",[1572,14659,14660,14663,14665],{"class":1574,"line":11787},[1572,14661,14662],{"class":1638},"        quantity: ",[1572,14664,9128],{"class":1644},[1572,14666,1655],{"class":1638},[1572,14668,14669],{"class":1574,"line":11793},[1572,14670,13649],{"class":1638},[1572,14672,14673],{"class":1574,"line":11806},[1572,14674,14675],{"class":1638},"    ],\n",[1572,14677,14678,14681,14684],{"class":1574,"line":11820},[1572,14679,14680],{"class":1638},"    mode: ",[1572,14682,14683],{"class":1651},"'subscription'",[1572,14685,1655],{"class":1638},[1572,14687,14688,14691,14694,14697,14699,14702,14704,14707,14710],{"class":1574,"line":11826},[1572,14689,14690],{"class":1638},"    success_url: ",[1572,14692,14693],{"class":1651},"`${",[1572,14695,14696],{"class":1638},"process",[1572,14698,507],{"class":1651},[1572,14700,14701],{"class":1638},"env",[1572,14703,507],{"class":1651},[1572,14705,14706],{"class":1644},"APP_URL",[1572,14708,14709],{"class":1651},"}\u002Fdashboard?billing=success`",[1572,14711,1655],{"class":1638},[1572,14713,14714,14717,14719,14721,14723,14725,14727,14729,14732],{"class":1574,"line":11831},[1572,14715,14716],{"class":1638},"    cancel_url: ",[1572,14718,14693],{"class":1651},[1572,14720,14696],{"class":1638},[1572,14722,507],{"class":1651},[1572,14724,14701],{"class":1638},[1572,14726,507],{"class":1651},[1572,14728,14706],{"class":1644},[1572,14730,14731],{"class":1651},"}\u002Fdashboard?billing=cancel`",[1572,14733,1655],{"class":1638},[1572,14735,14736],{"class":1574,"line":11838},[1572,14737,14738],{"class":1638},"    customer_email: input.email,\n",[1572,14740,14741],{"class":1574,"line":11890},[1572,14742,14743],{"class":1638},"    metadata: {\n",[1572,14745,14746],{"class":1574,"line":11895},[1572,14747,14748],{"class":1638},"      tenantId: input.tenantId,\n",[1572,14750,14751],{"class":1574,"line":11901},[1572,14752,13578],{"class":1638},[1572,14754,14755],{"class":1574,"line":11914},[1572,14756,12630],{"class":1638},[1572,14758,14759],{"class":1574,"line":11956},[1572,14760,10309],{"emptyLinePlaceholder":396},[1572,14762,14763,14765,14767,14769],{"class":1574,"line":11976},[1572,14764,11796],{"class":11551},[1572,14766,4110],{"class":1638},[1572,14768,5583],{"class":11551},[1572,14770,14771],{"class":1638},"session.url) {\n",[1572,14773,14774,14777,14779,14782,14784,14787],{"class":1574,"line":11981},[1572,14775,14776],{"class":11551},"    throw",[1572,14778,11754],{"class":11551},[1572,14780,14781],{"class":6311}," Error",[1572,14783,11715],{"class":1638},[1572,14785,14786],{"class":1651},"'Failed to generate Stripe checkout URL.'",[1572,14788,11784],{"class":1638},[1572,14790,14791],{"class":1574,"line":11987},[1572,14792,11823],{"class":1638},[1572,14794,14795],{"class":1574,"line":11993},[1572,14796,10309],{"emptyLinePlaceholder":396},[1572,14798,14799,14801],{"class":1574,"line":12006},[1572,14800,12639],{"class":11551},[1572,14802,14803],{"class":1638}," session.url;\n",[1572,14805,14806],{"class":1574,"line":12026},[1572,14807,1670],{"class":1638},[15,14809,14811],{"id":14810},"_4-launching-in-30-days-the-timeline","4. Launching in 30 Days: The Timeline",[298,14813,14814,14820,14826,14832,14838],{},[301,14815,14816,14819],{},[26,14817,14818],{},"Days 1–7 (Foundation)",": Schema migrations, basic router layouts, and session authentication using HTTP-only cookies.",[301,14821,14822,14825],{},[26,14823,14824],{},"Days 8–18 (Core Value)",": Build the primary feature (e.g., generating PDFs, scheduling posts) and hook it up to the UI.",[301,14827,14828,14831],{},[26,14829,14830],{},"Days 19–24 (Monetization)",": Integrate Stripe Checkout sessions, configure Stripe webhooks, and map webhook events to database subscription states.",[301,14833,14834,14837],{},[26,14835,14836],{},"Days 25–28 (Quality & Hardening)",": Write unit tests for critical business logic (e.g., checking if user has billing limits) and configure server monitoring.",[301,14839,14840,14843],{},[26,14841,14842],{},"Days 29–30 (Deployment & Analytics)",": Deploy to VPS using Docker, configure SSL, set up Yandex Metrika\u002FGoogle Analytics, and launch.",[15,14845,12723],{"id":12722},[298,14847,14848,14856,14864],{},[301,14849,14850,14855],{},[38,14851,14854],{"href":14852,"rel":14853},"https:\u002F\u002Fnuxt.com\u002Fdocs",[429],"Nuxt documentation"," — SSR, routing, and Nitro server endpoints",[301,14857,14858,14863],{},[38,14859,14862],{"href":14860,"rel":14861},"https:\u002F\u002Fwww.sqlite.org\u002Fwhentouse.html",[429],"SQLite: Appropriate Uses"," — official guidance on when SQLite fits production",[301,14865,14866,14870],{},[38,14867,14389],{"href":14868,"rel":14869},"https:\u002F\u002Fdocs.stripe.com\u002Fpayments\u002Fcheckout",[429]," — the hosted payment flow used in the example above",[11,14872,14873,14874,13330,14876,14878],{},"If you are planning to build a high-performance B2B SaaS MVP and need an experienced senior architect to launch it within weeks, explore my ",[38,14875,13451],{"href":13817},[38,14877,13333],{"href":12755}," to audit your product architecture.",[1779,14880,14881],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":14883},[14884,14885,14889,14895,14898,14899],{"id":14272,"depth":364,"text":14273},{"id":14288,"depth":364,"text":14289,"children":14886},[14887,14888],{"id":14302,"depth":370,"text":14303},{"id":14333,"depth":370,"text":14334},{"id":14368,"depth":364,"text":14369,"children":14890},[14891,14892,14893,14894],{"id":14375,"depth":370,"text":14376},{"id":14382,"depth":370,"text":14383},{"id":14410,"depth":370,"text":14411},{"id":14417,"depth":370,"text":14418},{"id":14424,"depth":364,"text":14425,"children":14896},[14897],{"id":14448,"depth":370,"text":14449},{"id":14810,"depth":364,"text":14811},{"id":12722,"depth":364,"text":12723},"2026-06-21","A practical guide for startup founders on launching a scalable B2B SaaS product quickly using a modern, lean tech stack, detailed backend modules, and optimal database layouts.",[14903,14906,14909],{"q":14904,"a":14905},"Can I really run production on SQLite?","Yes. SQLite is production-ready: with WAL (Write-Ahead Logging) mode enabled it reads faster than many client-server databases. For a bootstrapped startup validating hypotheses it's an ideal choice that minimizes hosting costs.",{"q":14907,"a":14908},"When should I migrate to PostgreSQL?","When you need horizontal scaling (multiple app servers — SQLite lives on a single disk) or when the database grows beyond ~100 GB. With an ORM or standard SQL, the migration takes hours, not weeks.",{"q":14910,"a":14911},"How much does an MVP of this scope cost?","In my practice, an MVP with authentication, Stripe billing, and an admin panel costs from $4,000 and takes 4–6 weeks from an approved specification. Half the budget goes into the product core — the feature customers pay for.","\u002Fimages\u002Fblog\u002Fblog_saas_mvp.webp",{},"\u002Fblog\u002Fen\u002Fb2b-saas-mvp-30-days",{"title":14257,"description":14901},"blog\u002Fen\u002Fb2b-saas-mvp-30-days",[12792,14251,14918,14919,12793],"Architecture","Nuxt","1PIcCAInsGmmor3ZDH-oEufztKbxcZsacNSxuAt8PxM",{"id":14922,"title":14923,"body":14924,"date":15201,"description":15202,"extension":376,"faq":15203,"image":15213,"lang":394,"meta":15214,"navigation":396,"path":15215,"published":396,"readTime":370,"seo":15216,"stem":15217,"tags":15218,"updated":12772,"__hash__":15223},"blog\u002Fblog\u002Fen\u002Fsaas-development-cheap-code-trap.md","The Trap of Cheap Development: When to Rewrite Your SaaS MVP",{"type":8,"value":14925,"toc":15192},[14926,14929,14932,14935,14938,14942,14945,14965,14972,14976,14979,15055,15059,15065,15071,15074,15112,15116,15134,15138,15141,15166,15168,15184],[11,14927,14928],{},"Minimizing initial development costs is a primary goal for almost every bootstrapped startup founder. This desire often leads to hiring cheap freelance agencies or low-cost offshore developers to assemble the first version (MVP) of their SaaS platform.",[11,14930,14931],{},"The promise is attractive: a full-featured B2B application delivered for a fraction of the cost of a senior developer’s salary.",[11,14933,14934],{},"However, a high percentage of these projects run into a technical wall shortly after launch. Adding a simple dropdown menu breaks the billing integration, the application load time exceeds 8 seconds, and new developers refuse to touch the codebase, advising you to \"rewrite everything from scratch.\"",[11,14936,14937],{},"In this management guide, we will analyze why the cheap development trap occurs, outline a technical rubric to diagnose if your codebase is salvageable, and look at the mathematical formula to choose between refactoring and rebuilding.",[15,14939,14941],{"id":14940},"the-economics-of-low-cost-development","The Economics of Low-Cost Development",[11,14943,14944],{},"When an agency charges $20-$30\u002Fhour to build a complex SaaS, they must optimize their own margins to survive. They achieve this in three ways:",[4001,14946,14947,14953,14959],{},[301,14948,14949,14952],{},[26,14950,14951],{},"Hiring Junior Developers",": The project is designed and written by engineers with minimal architectural experience.",[301,14954,14955,14958],{},[26,14956,14957],{},"Skipping Quality Controls",": Zero unit tests, integration tests, or API documentations are created. The only quality assurance is manual testing by the developers themselves.",[301,14960,14961,14964],{},[26,14962,14963],{},"Using Rigid Boilerplates",": The application is built on top of outdated templates or custom-patched WordPress\u002FNo-Code systems that are impossible to scale or integrate with modern APIs.",[11,14966,14967,14968,14971],{},"The result is massive ",[26,14969,14970],{},"technical debt"," accumulated from day one. You save money upfront, but you pay a high premium in developer friction and slow features velocity post-launch.",[15,14973,14975],{"id":14974},"_1-technical-health-rubric","1. Technical Health Rubric",[11,14977,14978],{},"To make an objective decision rather than relying on gut feelings, perform a technical check of your MVP against these key metrics:",[75,14980,14981,14994],{},[78,14982,14983],{},[81,14984,14985,14988,14991],{},[84,14986,14987],{"align":12854},"Engineering Layer",[84,14989,14990],{"align":12854},"Refactoring is Viable",[84,14992,14993],{"align":12854},"Rewrite is Mandatory",[91,14995,14996,15009,15022,15038],{},[81,14997,14998,15003,15006],{},[96,14999,15000],{"align":12854},[26,15001,15002],{},"Database Structure",[96,15004,15005],{"align":12854},"Relational schemas are normalized, with indexing on primary keys and foreign keys.",[96,15007,15008],{"align":12854},"No foreign keys, massive duplicate data columns, lack of transactional integrity.",[81,15010,15011,15016,15019],{},[96,15012,15013],{"align":12854},[26,15014,15015],{},"Code Isolation",[96,15017,15018],{"align":12854},"Logic is separated into controllers or services; UI components only handle display.",[96,15020,15021],{"align":12854},"Raw SQL queries or API credentials (like Stripe secret keys) are written inside client-side components.",[81,15023,15024,15029,15032],{},[96,15025,15026],{"align":12854},[26,15027,15028],{},"Dependency Health",[96,15030,15031],{"align":12854},"Standard packages (e.g. standard Node.js libraries) that can be upgraded via npm.",[96,15033,15034,15035,507],{"align":12854},"Proprietary, discontinued, or undocumented modules modified manually inside ",[49,15036,15037],{},"node_modules",[81,15039,15040,15045,15048],{},[96,15041,15042],{"align":12854},[26,15043,15044],{},"Error Handling",[96,15046,15047],{"align":12854},"Errors are caught, logged, and return clean HTTP status codes (400, 401, 500).",[96,15049,15050,15051,15054],{"align":12854},"Empty catch blocks (",[49,15052,15053],{},"try {} catch(e) {}",") that hide bugs, application crashes silently.",[15,15056,15058],{"id":15057},"_2-refactor-vs-rebuild-the-financial-calculation","2. Refactor vs. Rebuild: The Financial Calculation",[11,15060,15061,15062,8342],{},"To determine the best path forward, calculate the ",[26,15063,15064],{},"Refactoring Coefficient (K)",[1564,15066,15069],{"className":15067,"code":15068,"language":4815},[4813],"K = (C_audit + C_tests + C_db + C_logic) \u002F C_rebuild\n",[49,15070,15068],{"__ignoreMap":363},[11,15072,15073],{},"Where:",[298,15075,15076,15082,15088,15094,15100,15106],{},[301,15077,15078,15081],{},[26,15079,15080],{},"K",": Refactoring Coefficient (the threshold of feasibility).",[301,15083,15084,15087],{},[26,15085,15086],{},"C_audit",": Cost of auditing and identifying all hidden bugs in the current legacy code.",[301,15089,15090,15093],{},[26,15091,15092],{},"C_tests",": Cost of writing unit\u002Fintegration tests to ensure refactoring does not break existing features.",[301,15095,15096,15099],{},[26,15097,15098],{},"C_db",": Cost of migrating and fixing the database schema without losing existing customer records.",[301,15101,15102,15105],{},[26,15103,15104],{},"C_logic",": Cost of isolating business logic into clean services.",[301,15107,15108,15111],{},[26,15109,15110],{},"C_rebuild",": Cost of rebuilding the same feature set from scratch using a modern, scalable architecture (like Nuxt 3 + Node.js).",[70,15113,15115],{"id":15114},"the-decision-rule","The Decision Rule:",[298,15117,15118,15125],{},[301,15119,15120,15121,15124],{},"If ",[26,15122,15123],{},"K \u003C 0.5",": Refactoring is the logical choice. The foundation is solid; it just needs cleanup.",[301,15126,15120,15127,1648,15130,15133],{},[26,15128,15129],{},"K >= 0.7",[26,15131,15132],{},"Rewrite the application",". Attempting to patch a deeply flawed codebase will cost more time and money than starting fresh. It is similar to trying to fix a crumbling house foundation—you will spend more patching cracks than you would rebuilding.",[15,15135,15137],{"id":15136},"_3-how-to-protect-your-next-development-run","3. How to Protect Your Next Development Run",[11,15139,15140],{},"If you decide to rebuild, implement these checks to prevent falling into the same trap again:",[298,15142,15143,15149,15155],{},[301,15144,15145,15148],{},[26,15146,15147],{},"Establish Code Ownership",": Ensure all source code is hosted on your GitHub organization from day one, with branch protection rules requiring code reviews before merge.",[301,15150,15151,15154],{},[26,15152,15153],{},"Mandate Static Analysis",": Require the team to use automated tools (like ESLint or Biome) and maintain at least 70% test coverage on critical business logic.",[301,15156,15157,15160,15161,15165],{},[26,15158,15159],{},"Perform Early Audits",": Do not wait until week 20 to review the code. Hire an independent technical consultant to perform a code audit in week 2 or 3 of development to catch architectural mistakes early; you can read my complete ",[38,15162,15164],{"href":15163},"\u002Fblog\u002Fcode-audit-technical-debt","Codebase Audit Checklist"," to learn how to structure this check.",[15,15167,12723],{"id":12722},[298,15169,15170,15178],{},[301,15171,15172,15177],{},[38,15173,15176],{"href":15174,"rel":15175},"https:\u002F\u002Fmartinfowler.com\u002Fbliki\u002FTechnicalDebt.html",[429],"Martin Fowler: Technical Debt"," — the canonical definition of technical debt and its quadrants",[301,15179,15180,15183],{},[38,15181,10613],{"href":12988,"rel":15182},[429]," — the vulnerabilities most often found in \"cheap\" codebases",[11,15185,15186,15187,13970,15189,15191],{},"If you are dealing with a slow, bug-ridden MVP, struggling with low developer velocity, or need a professional assessment of your codebase's health, visit my ",[38,15188,13451],{"href":13817},[38,15190,13333],{"href":12755}," to receive an audit.",{"title":363,"searchDepth":364,"depth":364,"links":15193},[15194,15195,15196,15199,15200],{"id":14940,"depth":364,"text":14941},{"id":14974,"depth":364,"text":14975},{"id":15057,"depth":364,"text":15058,"children":15197},[15198],{"id":15114,"depth":370,"text":15115},{"id":15136,"depth":364,"text":15137},{"id":12722,"depth":364,"text":12723},"2026-06-18","Why hiring low-cost outsourcing agencies often results in rewriting your B2B SaaS MVP from scratch, and how to mathematically evaluate if a codebase is salvageable.",[15204,15207,15210],{"q":15205,"a":15206},"How do I quickly tell whether an MVP will need a rewrite?","Four red flags: no relations or indexes in the database, business logic and secret keys sitting inside client-side components, dependencies that are abandoned or hand-patched in node_modules, and errors silenced by empty try\u002Fcatch blocks with no logging. Two or more flags almost always means a rewrite.",{"q":15208,"a":15209},"When is refactoring cheaper than rebuilding?","When the combined cost of the audit, tests, database fixes, and logic extraction is under half the cost of building from scratch (coefficient K_R \u003C 0.5). At K_R ≥ 0.7 a rewrite wins: patching a crumbling foundation consumes more resources than pouring a new one.",{"q":15211,"a":15212},"How do I avoid the same trap with the next contractor?","Keep the code in your own repository with protected main and mandatory reviews, require linters and 70%+ test coverage on critical logic, and order an independent architecture audit in week 2–3 of development — not after release.","\u002Fimages\u002Fblog\u002Fblog_cheap_code_trap.webp",{},"\u002Fblog\u002Fen\u002Fsaas-development-cheap-code-trap",{"title":14923,"description":15202},"blog\u002Fen\u002Fsaas-development-cheap-code-trap",[12792,15219,15220,15221,15222],"Startup","Consultations","Management","Outsourcing","_PASDyxTO3fqAl5rhrg633j7yuYMsg2uwVjz2OhqcLE",{"id":15225,"title":15226,"body":15227,"date":16559,"description":16560,"extension":376,"faq":16561,"image":16571,"lang":394,"meta":16572,"navigation":396,"path":16573,"published":396,"readTime":1591,"seo":16574,"stem":16575,"tags":16576,"updated":12772,"__hash__":16580},"blog\u002Fblog\u002Fen\u002Fsaas-stripe-billing-integration.md","SaaS Monetization: Implementing Stripe, Billing, and Subscriptions Without Pain",{"type":8,"value":15228,"toc":16552},[15229,15232,15235,15238,15242,15248,15251,15257,15287,15291,15306,15309,16461,16465,16491,16501,16503,16529,16531,16534,16540,16549],[11,15230,15231],{},"Integrating recurring payments into a SaaS is notorious for being deceptively complex. While charging a credit card once is relatively straightforward, managing subscription lifecycles introduces a web of edge cases: handling card expirations, retry intervals, tier upgrades, plan downgrades, grace periods for failed payments, and webhooks processing.",[11,15233,15234],{},"If a webhook fails or is handled out of order, you might block paying users or grant free access to canceled subscribers.",[11,15236,15237],{},"In this comprehensive playbook, we will implement a production-ready, signature-verified Stripe webhook handler in Node.js\u002FTypeScript and map out the complete database synchronization lifecycle.",[15,15239,15241],{"id":15240},"_1-the-anatomy-of-subscription-lifecycles","1. The Anatomy of Subscription Lifecycles",[11,15243,15244],{},[1068,15245],{"alt":15246,"src":15247},"Stripe subscription state machine and webhook lifecycle","\u002Fimages\u002Fblog\u002Finline_saas-stripe-billing-integration.webp",[11,15249,15250],{},"Your database must track the exact status of a tenant's subscription. Stripe maps these to several statuses, which you should mirror in your schema:",[1564,15252,15255],{"className":15253,"code":15254,"language":4815,"meta":363},[4813],"               +--------------------------------------+\n               |               trialing               |\n               +------------------+-------------------+\n                                  | (Trial ends or user upgrades)\n                                  v\n+------------------->  +----------------------+\n|                      |        active        | \u003C------------------+\n|                      +----------+-----------+                    |\n| (Card updated,                  |                                |\n|  charge succeeds)               | (Payment fails, Stripe retries) | (User renews\n|                                 v                                |  before end)\n|                      +----------+-----------+                    |\n+---------------------+|       past_due       |                    |\n                       +----------+-----------+                    |\n                                  | (All retries fail \u002F period ends) |\n                                  v                                |\n                       +----------+-----------+                    |\n                       |       canceled       | -------------------+\n                       +----------------------+\n",[49,15256,15254],{"__ignoreMap":363},[298,15258,15259,15265,15271,15281],{},[301,15260,15261,15264],{},[49,15262,15263],{},"trialing",": User has full access without being charged yet.",[301,15266,15267,15270],{},[49,15268,15269],{},"active",": Successful payment. Full access.",[301,15272,15273,15276,15277,15280],{},[49,15274,15275],{},"past_due",": The card charge failed. ",[1321,15278,15279],{},"Do not block the user instantly",". Put the account into a warning state (grace period of 3 to 7 days), disable high-cost API calls, and email them a link to update their card.",[301,15282,15283,15286],{},[49,15284,15285],{},"canceled",": Access is revoked. The user must navigate to the portal to resubscribe.",[15,15288,15290],{"id":15289},"_2-implementing-a-secure-production-ready-webhook-handler","2. Implementing a Secure, Production-Ready Webhook Handler",[11,15292,15293,15294,15297,15298,15301,15302,15305],{},"Stripe sends asynchronous events to your API using HTTP POST requests. Because anyone can forge an HTTP request to your ",[49,15295,15296],{},"\u002Fapi\u002Fbilling\u002Fwebhook"," endpoint, you ",[26,15299,15300],{},"must verify the cryptographic signature"," sent by Stripe in the ",[49,15303,15304],{},"stripe-signature"," header.",[11,15307,15308],{},"Here is a robust implementation using Express and TypeScript:",[1564,15310,15312],{"className":11542,"code":15311,"language":11544,"meta":363,"style":363},"import express from 'express';\nimport Stripe from 'stripe';\n\nconst stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {\n  apiVersion: '2023-10-16',\n});\n\nconst app = express();\nconst endpointSecret = process.env.STRIPE_WEBHOOK_SECRET!;\n\n\u002F\u002F IMPORTANT: Webhook signature verification requires the raw request body buffer.\n\u002F\u002F Do not parse the body with standard express.json() parser before this route.\napp.post(\n  '\u002Fapi\u002Fbilling\u002Fwebhook',\n  express.raw({ type: 'application\u002Fjson' }),\n  async (req: express.Request, res: express.Response) => {\n    const sig = req.headers['stripe-signature'];\n    \n    if (!sig) {\n      return res.status(400).send('Missing stripe-signature header.');\n    }\n\n    let event: Stripe.Event;\n\n    try {\n      \u002F\u002F Validate signature against Stripe webhook secret\n      event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);\n    } catch (err: any) {\n      console.error(`[Webhook Security Error] Signature verification failed: ${err.message}`);\n      return res.status(400).send(`Webhook Error: ${err.message}`);\n    }\n\n    try {\n      switch (event.type) {\n        case 'customer.subscription.created':\n        case 'customer.subscription.updated': {\n          const subscription = event.data.object as Stripe.Subscription;\n          await handleSubscriptionUpdate(subscription);\n          break;\n        }\n        case 'customer.subscription.deleted': {\n          const subscription = event.data.object as Stripe.Subscription;\n          await handleSubscriptionDeletion(subscription);\n          break;\n        }\n        case 'invoice.payment_failed': {\n          const invoice = event.data.object as Stripe.Invoice;\n          await handlePaymentFailure(invoice);\n          break;\n        }\n        default:\n          console.log(`[Stripe Webhook] Ignored event: ${event.type}`);\n      }\n\n      res.json({ received: true });\n    } catch (err: any) {\n      console.error(`[Webhook Error] Processing failed: ${err.message}`);\n      res.status(500).send('Internal Server Error');\n    }\n  }\n);\n\nasync function handleSubscriptionUpdate(sub: Stripe.Subscription) {\n  const tenantId = sub.metadata.tenantId;\n  if (!tenantId) {\n    throw new Error(`Subscription ${sub.id} is missing tenantId metadata.`);\n  }\n\n  const status = sub.status; \u002F\u002F 'active', 'trialing', 'past_due', etc.\n  const priceId = sub.items.data[0].price.id;\n  const currentPeriodEnd = new Date(sub.current_period_end * 1000);\n\n  \u002F\u002F Sync to database\n  await db.query(\n    `UPDATE tenants \n     SET subscription_status = $1, stripe_price_id = $2, subscription_period_end = $3\n     WHERE id = $4`,\n    [status, priceId, currentPeriodEnd, tenantId]\n  );\n\n  console.log(`[Billing Sync] Updated tenant ${tenantId} to status: ${status}`);\n}\n\nasync function handleSubscriptionDeletion(sub: Stripe.Subscription) {\n  const tenantId = sub.metadata.tenantId;\n  if (!tenantId) return;\n\n  await db.query(\n    `UPDATE tenants \n     SET subscription_status = 'canceled', stripe_price_id = NULL\n     WHERE id = $1`,\n    [tenantId]\n  );\n\n  console.log(`[Billing Sync] Revoked subscription access for tenant ${tenantId}`);\n}\n\nasync function handlePaymentFailure(invoice: Stripe.Invoice) {\n  const customerId = invoice.customer as string;\n  \u002F\u002F Fetch user details from customerId and trigger alert emails\n  console.warn(`[Billing Alert] Payment failed for customer ${customerId}`);\n}\n",[49,15313,15314,15328,15340,15344,15364,15372,15376,15380,15394,15413,15417,15422,15427,15438,15445,15462,15502,15520,15525,15536,15564,15568,15572,15591,15595,15602,15607,15623,15642,15666,15697,15701,15705,15711,15719,15730,15739,15763,15774,15781,15785,15794,15814,15823,15829,15833,15842,15864,15874,15880,15884,15891,15916,15920,15924,15939,15955,15976,15998,16002,16006,16010,16015,16040,16053,16065,16091,16096,16101,16117,16135,16160,16165,16171,16185,16191,16197,16205,16211,16217,16222,16247,16252,16257,16280,16291,16308,16313,16324,16329,16335,16343,16349,16354,16359,16377,16382,16387,16411,16430,16436,16456],{"__ignoreMap":363},[1572,15315,15316,15318,15321,15323,15326],{"class":1574,"line":1575},[1572,15317,5892],{"class":11551},[1572,15319,15320],{"class":1638}," express ",[1572,15322,11557],{"class":11551},[1572,15324,15325],{"class":1651}," 'express'",[1572,15327,11563],{"class":1638},[1572,15329,15330,15332,15334,15336,15338],{"class":1574,"line":364},[1572,15331,5892],{"class":11551},[1572,15333,14469],{"class":1638},[1572,15335,11557],{"class":11551},[1572,15337,14474],{"class":1651},[1572,15339,11563],{"class":1638},[1572,15341,15342],{"class":1574,"line":370},[1572,15343,10309],{"emptyLinePlaceholder":396},[1572,15345,15346,15348,15350,15352,15354,15356,15358,15360,15362],{"class":1574,"line":1591},[1572,15347,5852],{"class":11551},[1572,15349,14487],{"class":1644},[1572,15351,11751],{"class":11551},[1572,15353,11754],{"class":11551},[1572,15355,14494],{"class":6311},[1572,15357,14497],{"class":1638},[1572,15359,14500],{"class":1644},[1572,15361,5583],{"class":11551},[1572,15363,14505],{"class":1638},[1572,15365,15366,15368,15370],{"class":1574,"line":1270},[1572,15367,14510],{"class":1638},[1572,15369,14513],{"class":1651},[1572,15371,1655],{"class":1638},[1572,15373,15374],{"class":1574,"line":4389},[1572,15375,13911],{"class":1638},[1572,15377,15378],{"class":1574,"line":742},[1572,15379,10309],{"emptyLinePlaceholder":396},[1572,15381,15382,15384,15387,15389,15392],{"class":1574,"line":10336},[1572,15383,5852],{"class":11551},[1572,15385,15386],{"class":1644}," app",[1572,15388,11751],{"class":11551},[1572,15390,15391],{"class":6311}," express",[1572,15393,11887],{"class":1638},[1572,15395,15396,15398,15401,15403,15406,15409,15411],{"class":1574,"line":10342},[1572,15397,5852],{"class":11551},[1572,15399,15400],{"class":1644}," endpointSecret",[1572,15402,11751],{"class":11551},[1572,15404,15405],{"class":1638}," process.env.",[1572,15407,15408],{"class":1644},"STRIPE_WEBHOOK_SECRET",[1572,15410,5583],{"class":11551},[1572,15412,11563],{"class":1638},[1572,15414,15415],{"class":1574,"line":10348},[1572,15416,10309],{"emptyLinePlaceholder":396},[1572,15418,15419],{"class":1574,"line":11656},[1572,15420,15421],{"class":11834},"\u002F\u002F IMPORTANT: Webhook signature verification requires the raw request body buffer.\n",[1572,15423,15424],{"class":1574,"line":11661},[1572,15425,15426],{"class":11834},"\u002F\u002F Do not parse the body with standard express.json() parser before this route.\n",[1572,15428,15429,15432,15435],{"class":1574,"line":11671},[1572,15430,15431],{"class":1638},"app.",[1572,15433,15434],{"class":6311},"post",[1572,15436,15437],{"class":1638},"(\n",[1572,15439,15440,15443],{"class":1574,"line":11683},[1572,15441,15442],{"class":1651},"  '\u002Fapi\u002Fbilling\u002Fwebhook'",[1572,15444,1655],{"class":1638},[1572,15446,15447,15450,15453,15456,15459],{"class":1574,"line":11695},[1572,15448,15449],{"class":1638},"  express.",[1572,15451,15452],{"class":6311},"raw",[1572,15454,15455],{"class":1638},"({ type: ",[1572,15457,15458],{"class":1651},"'application\u002Fjson'",[1572,15460,15461],{"class":1638}," }),\n",[1572,15463,15464,15467,15469,15472,15474,15476,15478,15481,15483,15486,15488,15490,15492,15495,15498,15500],{"class":1574,"line":11700},[1572,15465,15466],{"class":11551},"  async",[1572,15468,4110],{"class":1638},[1572,15470,15471],{"class":11583},"req",[1572,15473,8342],{"class":11551},[1572,15475,15391],{"class":6311},[1572,15477,507],{"class":1638},[1572,15479,15480],{"class":6311},"Request",[1572,15482,673],{"class":1638},[1572,15484,15485],{"class":11583},"res",[1572,15487,8342],{"class":11551},[1572,15489,15391],{"class":6311},[1572,15491,507],{"class":1638},[1572,15493,15494],{"class":6311},"Response",[1572,15496,15497],{"class":1638},") ",[1572,15499,12459],{"class":11551},[1572,15501,11578],{"class":1638},[1572,15503,15504,15506,15509,15511,15514,15517],{"class":1574,"line":11705},[1572,15505,12193],{"class":11551},[1572,15507,15508],{"class":1644}," sig",[1572,15510,11751],{"class":11551},[1572,15512,15513],{"class":1638}," req.headers[",[1572,15515,15516],{"class":1651},"'stripe-signature'",[1572,15518,15519],{"class":1638},"];\n",[1572,15521,15522],{"class":1574,"line":11742},[1572,15523,15524],{"class":1638},"    \n",[1572,15526,15527,15529,15531,15533],{"class":1574,"line":11763},[1572,15528,12487],{"class":11551},[1572,15530,4110],{"class":1638},[1572,15532,5583],{"class":11551},[1572,15534,15535],{"class":1638},"sig) {\n",[1572,15537,15538,15541,15544,15547,15549,15552,15554,15557,15559,15562],{"class":1574,"line":11787},[1572,15539,15540],{"class":11551},"      return",[1572,15542,15543],{"class":1638}," res.",[1572,15545,15546],{"class":6311},"status",[1572,15548,11715],{"class":1638},[1572,15550,15551],{"class":1644},"400",[1572,15553,8399],{"class":1638},[1572,15555,15556],{"class":6311},"send",[1572,15558,11715],{"class":1638},[1572,15560,15561],{"class":1651},"'Missing stripe-signature header.'",[1572,15563,11784],{"class":1638},[1572,15565,15566],{"class":1574,"line":11793},[1572,15567,12625],{"class":1638},[1572,15569,15570],{"class":1574,"line":11806},[1572,15571,10309],{"emptyLinePlaceholder":396},[1572,15573,15574,15577,15580,15582,15584,15586,15589],{"class":1574,"line":11820},[1572,15575,15576],{"class":11551},"    let",[1572,15578,15579],{"class":1638}," event",[1572,15581,8342],{"class":11551},[1572,15583,14494],{"class":6311},[1572,15585,507],{"class":1638},[1572,15587,15588],{"class":6311},"Event",[1572,15590,11563],{"class":1638},[1572,15592,15593],{"class":1574,"line":11826},[1572,15594,10309],{"emptyLinePlaceholder":396},[1572,15596,15597,15600],{"class":1574,"line":11831},[1572,15598,15599],{"class":11551},"    try",[1572,15601,11578],{"class":1638},[1572,15603,15604],{"class":1574,"line":11838},[1572,15605,15606],{"class":11834},"      \u002F\u002F Validate signature against Stripe webhook secret\n",[1572,15608,15609,15612,15614,15617,15620],{"class":1574,"line":11890},[1572,15610,15611],{"class":1638},"      event ",[1572,15613,6315],{"class":11551},[1572,15615,15616],{"class":1638}," stripe.webhooks.",[1572,15618,15619],{"class":6311},"constructEvent",[1572,15621,15622],{"class":1638},"(req.body, sig, endpointSecret);\n",[1572,15624,15625,15628,15630,15632,15635,15637,15639],{"class":1574,"line":11895},[1572,15626,15627],{"class":1638},"    } ",[1572,15629,12267],{"class":11551},[1572,15631,4110],{"class":1638},[1572,15633,15634],{"class":11583},"err",[1572,15636,8342],{"class":11551},[1572,15638,12479],{"class":1644},[1572,15640,15641],{"class":1638},") {\n",[1572,15643,15644,15647,15650,15652,15655,15657,15659,15662,15664],{"class":1574,"line":11901},[1572,15645,15646],{"class":1638},"      console.",[1572,15648,15649],{"class":6311},"error",[1572,15651,11715],{"class":1638},[1572,15653,15654],{"class":1651},"`[Webhook Security Error] Signature verification failed: ${",[1572,15656,15634],{"class":1638},[1572,15658,507],{"class":1651},[1572,15660,15661],{"class":1638},"message",[1572,15663,11950],{"class":1651},[1572,15665,11784],{"class":1638},[1572,15667,15668,15670,15672,15674,15676,15678,15680,15682,15684,15687,15689,15691,15693,15695],{"class":1574,"line":11914},[1572,15669,15540],{"class":11551},[1572,15671,15543],{"class":1638},[1572,15673,15546],{"class":6311},[1572,15675,11715],{"class":1638},[1572,15677,15551],{"class":1644},[1572,15679,8399],{"class":1638},[1572,15681,15556],{"class":6311},[1572,15683,11715],{"class":1638},[1572,15685,15686],{"class":1651},"`Webhook Error: ${",[1572,15688,15634],{"class":1638},[1572,15690,507],{"class":1651},[1572,15692,15661],{"class":1638},[1572,15694,11950],{"class":1651},[1572,15696,11784],{"class":1638},[1572,15698,15699],{"class":1574,"line":11956},[1572,15700,12625],{"class":1638},[1572,15702,15703],{"class":1574,"line":11976},[1572,15704,10309],{"emptyLinePlaceholder":396},[1572,15706,15707,15709],{"class":1574,"line":11981},[1572,15708,15599],{"class":11551},[1572,15710,11578],{"class":1638},[1572,15712,15713,15716],{"class":1574,"line":11987},[1572,15714,15715],{"class":11551},"      switch",[1572,15717,15718],{"class":1638}," (event.type) {\n",[1572,15720,15721,15724,15727],{"class":1574,"line":11993},[1572,15722,15723],{"class":11551},"        case",[1572,15725,15726],{"class":1651}," 'customer.subscription.created'",[1572,15728,15729],{"class":1638},":\n",[1572,15731,15732,15734,15737],{"class":1574,"line":12006},[1572,15733,15723],{"class":11551},[1572,15735,15736],{"class":1651}," 'customer.subscription.updated'",[1572,15738,13081],{"class":1638},[1572,15740,15741,15744,15747,15749,15752,15754,15756,15758,15761],{"class":1574,"line":12026},[1572,15742,15743],{"class":11551},"          const",[1572,15745,15746],{"class":1644}," subscription",[1572,15748,11751],{"class":11551},[1572,15750,15751],{"class":1638}," event.data.object ",[1572,15753,12476],{"class":11551},[1572,15755,14494],{"class":6311},[1572,15757,507],{"class":1638},[1572,15759,15760],{"class":6311},"Subscription",[1572,15762,11563],{"class":1638},[1572,15764,15765,15768,15771],{"class":1574,"line":12037},[1572,15766,15767],{"class":11551},"          await",[1572,15769,15770],{"class":6311}," handleSubscriptionUpdate",[1572,15772,15773],{"class":1638},"(subscription);\n",[1572,15775,15776,15779],{"class":1574,"line":12047},[1572,15777,15778],{"class":11551},"          break",[1572,15780,11563],{"class":1638},[1572,15782,15783],{"class":1574,"line":12052},[1572,15784,13757],{"class":1638},[1572,15786,15787,15789,15792],{"class":1574,"line":12058},[1572,15788,15723],{"class":11551},[1572,15790,15791],{"class":1651}," 'customer.subscription.deleted'",[1572,15793,13081],{"class":1638},[1572,15795,15796,15798,15800,15802,15804,15806,15808,15810,15812],{"class":1574,"line":12070},[1572,15797,15743],{"class":11551},[1572,15799,15746],{"class":1644},[1572,15801,11751],{"class":11551},[1572,15803,15751],{"class":1638},[1572,15805,12476],{"class":11551},[1572,15807,14494],{"class":6311},[1572,15809,507],{"class":1638},[1572,15811,15760],{"class":6311},[1572,15813,11563],{"class":1638},[1572,15815,15816,15818,15821],{"class":1574,"line":12084},[1572,15817,15767],{"class":11551},[1572,15819,15820],{"class":6311}," handleSubscriptionDeletion",[1572,15822,15773],{"class":1638},[1572,15824,15825,15827],{"class":1574,"line":12094},[1572,15826,15778],{"class":11551},[1572,15828,11563],{"class":1638},[1572,15830,15831],{"class":1574,"line":12108},[1572,15832,13757],{"class":1638},[1572,15834,15835,15837,15840],{"class":1574,"line":12113},[1572,15836,15723],{"class":11551},[1572,15838,15839],{"class":1651}," 'invoice.payment_failed'",[1572,15841,13081],{"class":1638},[1572,15843,15844,15846,15849,15851,15853,15855,15857,15859,15862],{"class":1574,"line":12119},[1572,15845,15743],{"class":11551},[1572,15847,15848],{"class":1644}," invoice",[1572,15850,11751],{"class":11551},[1572,15852,15751],{"class":1638},[1572,15854,12476],{"class":11551},[1572,15856,14494],{"class":6311},[1572,15858,507],{"class":1638},[1572,15860,15861],{"class":6311},"Invoice",[1572,15863,11563],{"class":1638},[1572,15865,15866,15868,15871],{"class":1574,"line":12138},[1572,15867,15767],{"class":11551},[1572,15869,15870],{"class":6311}," handlePaymentFailure",[1572,15872,15873],{"class":1638},"(invoice);\n",[1572,15875,15876,15878],{"class":1574,"line":12143},[1572,15877,15778],{"class":11551},[1572,15879,11563],{"class":1638},[1572,15881,15882],{"class":1574,"line":12155},[1572,15883,13757],{"class":1638},[1572,15885,15886,15889],{"class":1574,"line":12166},[1572,15887,15888],{"class":11551},"        default",[1572,15890,15729],{"class":1638},[1572,15892,15893,15896,15899,15901,15904,15907,15909,15912,15914],{"class":1574,"line":12171},[1572,15894,15895],{"class":1638},"          console.",[1572,15897,15898],{"class":6311},"log",[1572,15900,11715],{"class":1638},[1572,15902,15903],{"class":1651},"`[Stripe Webhook] Ignored event: ${",[1572,15905,15906],{"class":1638},"event",[1572,15908,507],{"class":1651},[1572,15910,15911],{"class":1638},"type",[1572,15913,11950],{"class":1651},[1572,15915,11784],{"class":1638},[1572,15917,15918],{"class":1574,"line":12176},[1572,15919,13261],{"class":1638},[1572,15921,15922],{"class":1574,"line":12182},[1572,15923,10309],{"emptyLinePlaceholder":396},[1572,15925,15926,15929,15931,15934,15936],{"class":1574,"line":12190},[1572,15927,15928],{"class":1638},"      res.",[1572,15930,1631],{"class":6311},[1572,15932,15933],{"class":1638},"({ received: ",[1572,15935,3638],{"class":1644},[1572,15937,15938],{"class":1638}," });\n",[1572,15940,15941,15943,15945,15947,15949,15951,15953],{"class":1574,"line":12212},[1572,15942,15627],{"class":1638},[1572,15944,12267],{"class":11551},[1572,15946,4110],{"class":1638},[1572,15948,15634],{"class":11583},[1572,15950,8342],{"class":11551},[1572,15952,12479],{"class":1644},[1572,15954,15641],{"class":1638},[1572,15956,15957,15959,15961,15963,15966,15968,15970,15972,15974],{"class":1574,"line":12249},[1572,15958,15646],{"class":1638},[1572,15960,15649],{"class":6311},[1572,15962,11715],{"class":1638},[1572,15964,15965],{"class":1651},"`[Webhook Error] Processing failed: ${",[1572,15967,15634],{"class":1638},[1572,15969,507],{"class":1651},[1572,15971,15661],{"class":1638},[1572,15973,11950],{"class":1651},[1572,15975,11784],{"class":1638},[1572,15977,15978,15980,15982,15984,15987,15989,15991,15993,15996],{"class":1574,"line":12261},[1572,15979,15928],{"class":1638},[1572,15981,15546],{"class":6311},[1572,15983,11715],{"class":1638},[1572,15985,15986],{"class":1644},"500",[1572,15988,8399],{"class":1638},[1572,15990,15556],{"class":6311},[1572,15992,11715],{"class":1638},[1572,15994,15995],{"class":1651},"'Internal Server Error'",[1572,15997,11784],{"class":1638},[1572,15999,16000],{"class":1574,"line":12273},[1572,16001,12625],{"class":1638},[1572,16003,16004],{"class":1574,"line":12284},[1572,16005,11823],{"class":1638},[1572,16007,16008],{"class":1574,"line":12289},[1572,16009,11784],{"class":1638},[1572,16011,16013],{"class":1574,"line":16012},62,[1572,16014,10309],{"emptyLinePlaceholder":396},[1572,16016,16018,16021,16023,16025,16027,16030,16032,16034,16036,16038],{"class":1574,"line":16017},63,[1572,16019,16020],{"class":11551},"async",[1572,16022,7934],{"class":11551},[1572,16024,15770],{"class":6311},[1572,16026,11715],{"class":1638},[1572,16028,16029],{"class":11583},"sub",[1572,16031,8342],{"class":11551},[1572,16033,14494],{"class":6311},[1572,16035,507],{"class":1638},[1572,16037,15760],{"class":6311},[1572,16039,15641],{"class":1638},[1572,16041,16043,16045,16048,16050],{"class":1574,"line":16042},64,[1572,16044,11745],{"class":11551},[1572,16046,16047],{"class":1644}," tenantId",[1572,16049,11751],{"class":11551},[1572,16051,16052],{"class":1638}," sub.metadata.tenantId;\n",[1572,16054,16056,16058,16060,16062],{"class":1574,"line":16055},65,[1572,16057,11796],{"class":11551},[1572,16059,4110],{"class":1638},[1572,16061,5583],{"class":11551},[1572,16063,16064],{"class":1638},"tenantId) {\n",[1572,16066,16068,16070,16072,16074,16076,16079,16081,16083,16086,16089],{"class":1574,"line":16067},66,[1572,16069,14776],{"class":11551},[1572,16071,11754],{"class":11551},[1572,16073,14781],{"class":6311},[1572,16075,11715],{"class":1638},[1572,16077,16078],{"class":1651},"`Subscription ${",[1572,16080,16029],{"class":1638},[1572,16082,507],{"class":1651},[1572,16084,16085],{"class":1638},"id",[1572,16087,16088],{"class":1651},"} is missing tenantId metadata.`",[1572,16090,11784],{"class":1638},[1572,16092,16094],{"class":1574,"line":16093},67,[1572,16095,11823],{"class":1638},[1572,16097,16099],{"class":1574,"line":16098},68,[1572,16100,10309],{"emptyLinePlaceholder":396},[1572,16102,16104,16106,16109,16111,16114],{"class":1574,"line":16103},69,[1572,16105,11745],{"class":11551},[1572,16107,16108],{"class":1644}," status",[1572,16110,11751],{"class":11551},[1572,16112,16113],{"class":1638}," sub.status; ",[1572,16115,16116],{"class":11834},"\u002F\u002F 'active', 'trialing', 'past_due', etc.\n",[1572,16118,16120,16122,16125,16127,16130,16132],{"class":1574,"line":16119},70,[1572,16121,11745],{"class":11551},[1572,16123,16124],{"class":1644}," priceId",[1572,16126,11751],{"class":11551},[1572,16128,16129],{"class":1638}," sub.items.data[",[1572,16131,1879],{"class":1644},[1572,16133,16134],{"class":1638},"].price.id;\n",[1572,16136,16138,16140,16143,16145,16147,16150,16153,16155,16158],{"class":1574,"line":16137},71,[1572,16139,11745],{"class":11551},[1572,16141,16142],{"class":1644}," currentPeriodEnd",[1572,16144,11751],{"class":11551},[1572,16146,11754],{"class":11551},[1572,16148,16149],{"class":6311}," Date",[1572,16151,16152],{"class":1638},"(sub.current_period_end ",[1572,16154,10847],{"class":11551},[1572,16156,16157],{"class":1644}," 1000",[1572,16159,11784],{"class":1638},[1572,16161,16163],{"class":1574,"line":16162},72,[1572,16164,10309],{"emptyLinePlaceholder":396},[1572,16166,16168],{"class":1574,"line":16167},73,[1572,16169,16170],{"class":11834},"  \u002F\u002F Sync to database\n",[1572,16172,16174,16177,16180,16183],{"class":1574,"line":16173},74,[1572,16175,16176],{"class":11551},"  await",[1572,16178,16179],{"class":1638}," db.",[1572,16181,16182],{"class":6311},"query",[1572,16184,15437],{"class":1638},[1572,16186,16188],{"class":1574,"line":16187},75,[1572,16189,16190],{"class":1651},"    `UPDATE tenants \n",[1572,16192,16194],{"class":1574,"line":16193},76,[1572,16195,16196],{"class":1651},"     SET subscription_status = $1, stripe_price_id = $2, subscription_period_end = $3\n",[1572,16198,16200,16203],{"class":1574,"line":16199},77,[1572,16201,16202],{"class":1651},"     WHERE id = $4`",[1572,16204,1655],{"class":1638},[1572,16206,16208],{"class":1574,"line":16207},78,[1572,16209,16210],{"class":1638},"    [status, priceId, currentPeriodEnd, tenantId]\n",[1572,16212,16214],{"class":1574,"line":16213},79,[1572,16215,16216],{"class":1638},"  );\n",[1572,16218,16220],{"class":1574,"line":16219},80,[1572,16221,10309],{"emptyLinePlaceholder":396},[1572,16223,16225,16228,16230,16232,16235,16238,16241,16243,16245],{"class":1574,"line":16224},81,[1572,16226,16227],{"class":1638},"  console.",[1572,16229,15898],{"class":6311},[1572,16231,11715],{"class":1638},[1572,16233,16234],{"class":1651},"`[Billing Sync] Updated tenant ${",[1572,16236,16237],{"class":1638},"tenantId",[1572,16239,16240],{"class":1651},"} to status: ${",[1572,16242,15546],{"class":1638},[1572,16244,11950],{"class":1651},[1572,16246,11784],{"class":1638},[1572,16248,16250],{"class":1574,"line":16249},82,[1572,16251,1670],{"class":1638},[1572,16253,16255],{"class":1574,"line":16254},83,[1572,16256,10309],{"emptyLinePlaceholder":396},[1572,16258,16260,16262,16264,16266,16268,16270,16272,16274,16276,16278],{"class":1574,"line":16259},84,[1572,16261,16020],{"class":11551},[1572,16263,7934],{"class":11551},[1572,16265,15820],{"class":6311},[1572,16267,11715],{"class":1638},[1572,16269,16029],{"class":11583},[1572,16271,8342],{"class":11551},[1572,16273,14494],{"class":6311},[1572,16275,507],{"class":1638},[1572,16277,15760],{"class":6311},[1572,16279,15641],{"class":1638},[1572,16281,16283,16285,16287,16289],{"class":1574,"line":16282},85,[1572,16284,11745],{"class":11551},[1572,16286,16047],{"class":1644},[1572,16288,11751],{"class":11551},[1572,16290,16052],{"class":1638},[1572,16292,16294,16296,16298,16300,16303,16306],{"class":1574,"line":16293},86,[1572,16295,11796],{"class":11551},[1572,16297,4110],{"class":1638},[1572,16299,5583],{"class":11551},[1572,16301,16302],{"class":1638},"tenantId) ",[1572,16304,16305],{"class":11551},"return",[1572,16307,11563],{"class":1638},[1572,16309,16311],{"class":1574,"line":16310},87,[1572,16312,10309],{"emptyLinePlaceholder":396},[1572,16314,16316,16318,16320,16322],{"class":1574,"line":16315},88,[1572,16317,16176],{"class":11551},[1572,16319,16179],{"class":1638},[1572,16321,16182],{"class":6311},[1572,16323,15437],{"class":1638},[1572,16325,16327],{"class":1574,"line":16326},89,[1572,16328,16190],{"class":1651},[1572,16330,16332],{"class":1574,"line":16331},90,[1572,16333,16334],{"class":1651},"     SET subscription_status = 'canceled', stripe_price_id = NULL\n",[1572,16336,16338,16341],{"class":1574,"line":16337},91,[1572,16339,16340],{"class":1651},"     WHERE id = $1`",[1572,16342,1655],{"class":1638},[1572,16344,16346],{"class":1574,"line":16345},92,[1572,16347,16348],{"class":1638},"    [tenantId]\n",[1572,16350,16352],{"class":1574,"line":16351},93,[1572,16353,16216],{"class":1638},[1572,16355,16357],{"class":1574,"line":16356},94,[1572,16358,10309],{"emptyLinePlaceholder":396},[1572,16360,16362,16364,16366,16368,16371,16373,16375],{"class":1574,"line":16361},95,[1572,16363,16227],{"class":1638},[1572,16365,15898],{"class":6311},[1572,16367,11715],{"class":1638},[1572,16369,16370],{"class":1651},"`[Billing Sync] Revoked subscription access for tenant ${",[1572,16372,16237],{"class":1638},[1572,16374,11950],{"class":1651},[1572,16376,11784],{"class":1638},[1572,16378,16380],{"class":1574,"line":16379},96,[1572,16381,1670],{"class":1638},[1572,16383,16385],{"class":1574,"line":16384},97,[1572,16386,10309],{"emptyLinePlaceholder":396},[1572,16388,16390,16392,16394,16396,16398,16401,16403,16405,16407,16409],{"class":1574,"line":16389},98,[1572,16391,16020],{"class":11551},[1572,16393,7934],{"class":11551},[1572,16395,15870],{"class":6311},[1572,16397,11715],{"class":1638},[1572,16399,16400],{"class":11583},"invoice",[1572,16402,8342],{"class":11551},[1572,16404,14494],{"class":6311},[1572,16406,507],{"class":1638},[1572,16408,15861],{"class":6311},[1572,16410,15641],{"class":1638},[1572,16412,16414,16416,16419,16421,16424,16426,16428],{"class":1574,"line":16413},99,[1572,16415,11745],{"class":11551},[1572,16417,16418],{"class":1644}," customerId",[1572,16420,11751],{"class":11551},[1572,16422,16423],{"class":1638}," invoice.customer ",[1572,16425,12476],{"class":11551},[1572,16427,11601],{"class":1644},[1572,16429,11563],{"class":1638},[1572,16431,16433],{"class":1574,"line":16432},100,[1572,16434,16435],{"class":11834},"  \u002F\u002F Fetch user details from customerId and trigger alert emails\n",[1572,16437,16439,16441,16444,16446,16449,16452,16454],{"class":1574,"line":16438},101,[1572,16440,16227],{"class":1638},[1572,16442,16443],{"class":6311},"warn",[1572,16445,11715],{"class":1638},[1572,16447,16448],{"class":1651},"`[Billing Alert] Payment failed for customer ${",[1572,16450,16451],{"class":1638},"customerId",[1572,16453,11950],{"class":1651},[1572,16455,11784],{"class":1638},[1572,16457,16459],{"class":1574,"line":16458},102,[1572,16460,1670],{"class":1638},[15,16462,16464],{"id":16463},"_3-best-practices-for-b2b-saas-billing-architectures","3. Best Practices for B2B SaaS Billing Architectures",[4001,16466,16467,16473,16485],{},[301,16468,16469,16472],{},[26,16470,16471],{},"Always use Stripe Checkout & Customer Portal",": Building customer interfaces for credit card management, invoices, and billing history is an unnecessary waste of resources. Stripe’s hosted pages are highly secure, PCI-compliant, and automatically localized.",[301,16474,16475,16478,16479,698,16481,16484],{},[26,16476,16477],{},"Pass IDs in Metadata",": When launching a Checkout Session, pass your database's ",[49,16480,16237],{},[49,16482,16483],{},"userId"," in the session's metadata. Stripe returns this metadata in webhook events, allowing you to associate payments with database records.",[301,16486,16487,16490],{},[26,16488,16489],{},"Idempotency",": Webhook deliveries can occasionally double-fire. Implement checks to ensure your database updates do not trigger duplicate processing logs or invoice generations.",[11,16492,16493,16494,16496,16497,507],{},"For example, in ",[38,16495,12706],{"href":12705},", we utilized this exact signature-verified webhook pipeline to process subscriptions safely and sync plan details in real time. We also paired it with Telegram Stars for in-app microtransactions, which you can read about in my guide on ",[38,16498,16500],{"href":16499},"\u002Fblog\u002Ftelegram-web-apps-sells","Telegram Web Apps SaaS Development",[15,16502,12723],{"id":12722},[298,16504,16505,16513,16521],{},[301,16506,16507,16512],{},[38,16508,16511],{"href":16509,"rel":16510},"https:\u002F\u002Fdocs.stripe.com\u002Fwebhooks",[429],"Stripe Webhooks"," — delivery, signatures, and retry behavior",[301,16514,16515,16520],{},[38,16516,16519],{"href":16517,"rel":16518},"https:\u002F\u002Fdocs.stripe.com\u002Fbilling\u002Fsubscriptions\u002Foverview",[429],"Stripe: subscription lifecycle"," — every status from the diagram above",[301,16522,16523,16528],{},[38,16524,16527],{"href":16525,"rel":16526},"https:\u002F\u002Fdocs.stripe.com\u002Fstripe-cli",[429],"Stripe CLI"," — local webhook testing",[15,16530,358],{"id":357},[357,16532],{":slug":16533},"[\"webhook-verifier\",\"telegram-stars-invoice\",\"api-sandbox\",\"saas-metrics-simulator\"]",[11,16535,16536,16537,507],{},"Building a secure, resilient billing engine is critical to protecting your B2B SaaS revenue stream and providing a premium customer experience. To learn how this payment layer fits into a rapid startup launch cycle, read my playbook on ",[38,16538,16539],{"href":14211},"Building a B2B SaaS MVP in 30 Days",[11,16541,16542,16543,16545,16546,16548],{},"If you are planning to build or refactor a billing integration in your SaaS application, learn more about my ",[38,16544,13451],{"href":13817}," or schedule a ",[38,16547,13333],{"href":12755}," to design a seamless payment pipeline.",[1779,16550,16551],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":16553},[16554,16555,16556,16557,16558],{"id":15240,"depth":364,"text":15241},{"id":15289,"depth":364,"text":15290},{"id":16463,"depth":364,"text":16464},{"id":12722,"depth":364,"text":12723},{"id":357,"depth":364,"text":358},"2026-06-14","A comprehensive technical playbook for integrating Stripe subscriptions, secure signature-verified webhooks, and grace period lifecycles into B2B SaaS applications.",[16562,16565,16568],{"q":16563,"a":16564},"Why can't I grant access right after the payment redirect?","A redirect to the success page doesn't guarantee the money was charged: the user may close the tab, or the payment may fail fraud checks. The single source of truth is Stripe webhooks: grant access on checkout.session.completed and extend it on invoice.paid.",{"q":16566,"a":16567},"What if a Stripe webhook never arrives?","Stripe retries event delivery for up to three days, so your handler must be idempotent — a repeated event must not duplicate emails or renewals. As extra insurance, run a scheduled background job that reconciles subscription statuses.",{"q":16569,"a":16570},"How do I test webhooks locally?","With the Stripe CLI: stripe listen forwards events to localhost, and stripe trigger generates test events of any type — from successful payments to failed charges. This lets you debug every handler branch before production.","\u002Fimages\u002Fblog\u002Fblog_stripe_billing.webp",{},"\u002Fblog\u002Fen\u002Fsaas-stripe-billing-integration",{"title":15226,"description":16560},"blog\u002Fen\u002Fsaas-stripe-billing-integration",[12792,16577,12793,16578,16579],"Payments","API","Stripe","xS5ZlMnPXpDHvOBxb5mHmyhcej_JTTEJa1qeh6hTPcQ",{"id":16582,"title":16583,"body":16584,"date":17272,"description":17273,"extension":376,"faq":17274,"image":17284,"lang":394,"meta":17285,"navigation":396,"path":17286,"published":396,"readTime":1591,"seo":17287,"stem":17288,"tags":17289,"updated":12772,"__hash__":17293},"blog\u002Fblog\u002Fen\u002Fsaas-multitenancy-databases.md","SaaS Database Architecture: Single-Tenant vs. Multi-Tenant Patterns",{"type":8,"value":16585,"toc":17261},[16586,16589,16592,16596,16602,16606,16612,16615,16753,16757,16764,16771,16775,16782,16885,16889,16895,16915,16919,16922,17180,17184,17210,17216,17218,17244,17250,17258],[11,16587,16588],{},"Choosing the database architecture for your B2B SaaS is one of the most critical decisions you will make. It directly impacts your infrastructure costs, development velocity, database migration complexity, and compliance with data security standards (such as GDPR, HIPAA, or SOC2).",[11,16590,16591],{},"A mistake here is incredibly expensive to rectify later. In this technical guide, we will analyze the three primary multi-tenant database patterns, map their trade-offs in detail, and write a complete, secure implementation using PostgreSQL Row-Level Security (RLS).",[15,16593,16595],{"id":16594},"the-three-multi-tenant-paradigms","The Three Multi-Tenant Paradigms",[1564,16597,16600],{"className":16598,"code":16599,"language":4815,"meta":363},[4813],"+-----------------------+   +-----------------------+   +-----------------------+\n|  Pattern A: Separate  |   |  Pattern B: Separate  |   |   Pattern C: Shared   |\n|       Databases       |   |        Schemas        |   |    Database & Tables  |\n+-----------------------+   +-----------------------+   +-----------------------+\n|  [DB 1]      [DB 2]   |   |  [Database]           |   |  [Database]           |\n|  Tenant A    Tenant B |   |   ├── Schema Tenant A |   |   └── [Table]         |\n|                       |   |   └── Schema Tenant B |   |        ├── Row Tenant A|\n| (Max Isolation\u002FCost)  |   | (Medium Isolation)    |   |        └── Row Tenant B|\n+-----------------------+   +-----------------------+   +-----------------------+\n",[49,16601,16599],{"__ignoreMap":363},[15,16603,16605],{"id":16604},"_1-deep-dive-comparison","1. Deep Dive Comparison",[11,16607,16608],{},[1068,16609],{"alt":16610,"src":16611},"Comparison of 3 SaaS database multitenancy architectures","\u002Fimages\u002Fblog\u002Finline_saas-multitenancy-databases.webp",[11,16613,16614],{},"To make an informed decision, let's contrast the three patterns across operational metrics:",[75,16616,16617,16632],{},[78,16618,16619],{},[81,16620,16621,16623,16626,16629],{},[84,16622,4045],{"align":12854},[84,16624,16625],{"align":12854},"Pattern A: Separate DBs",[84,16627,16628],{"align":12854},"Pattern B: Separate Schemas",[84,16630,16631],{"align":12854},"Pattern C: Shared Table (RLS)",[91,16633,16634,16659,16682,16706,16730],{},[81,16635,16636,16641,16647,16653],{},[96,16637,16638],{"align":12854},[26,16639,16640],{},"Data Isolation",[96,16642,16643,16646],{"align":12854},[26,16644,16645],{},"Highest",": Zero risk of cross-tenant leakage.",[96,16648,16649,16652],{"align":12854},[26,16650,16651],{},"High",": Logic-level namespace isolation.",[96,16654,16655,16658],{"align":12854},[26,16656,16657],{},"Medium",": Relies on SQL query filters or database policies.",[81,16660,16661,16666,16671,16676],{},[96,16662,16663],{"align":12854},[26,16664,16665],{},"Infrastructure Cost",[96,16667,16668,16670],{"align":12854},[26,16669,16645],{},": Resource overhead for running multiple DB servers or pools.",[96,16672,16673,16675],{"align":12854},[26,16674,16657],{},": Shared hardware, catalog memory limits.",[96,16677,16678,16681],{"align":12854},[26,16679,16680],{},"Lowest",": Maximum utilization of database resources.",[81,16683,16684,16689,16695,16700],{},[96,16685,16686],{"align":12854},[26,16687,16688],{},"Migrations Complexity",[96,16690,16691,16694],{"align":12854},[26,16692,16693],{},"Very High",": Need to run migration scripts on hundreds of DBs in sequence.",[96,16696,16697,16699],{"align":12854},[26,16698,16651],{},": Iterating updates through hundreds of schemas.",[96,16701,16702,16705],{"align":12854},[26,16703,16704],{},"Simple",": Single standard migration run.",[81,16707,16708,16713,16719,16724],{},[96,16709,16710],{"align":12854},[26,16711,16712],{},"Connection Pooling",[96,16714,16715,16718],{"align":12854},[26,16716,16717],{},"Hard",": Separate connection pool required per database.",[96,16720,16721,16723],{"align":12854},[26,16722,16657],{},": Can use one pool but requires schema-switching commands.",[96,16725,16726,16729],{"align":12854},[26,16727,16728],{},"Easiest",": Single connection pool shared by all tenants.",[81,16731,16732,16737,16743,16748],{},[96,16733,16734],{"align":12854},[26,16735,16736],{},"Backup & Restore",[96,16738,16739,16742],{"align":12854},[26,16740,16741],{},"Perfect",": Can easily back up or restore a single customer.",[96,16744,16745,16747],{"align":12854},[26,16746,16657],{},": Requires selective pg_dump\u002Fpg_restore of schemas.",[96,16749,16750,16752],{"align":12854},[26,16751,16717],{},": Restoring one client requires filtering logs or data exports.",[15,16754,16756],{"id":16755},"_2-implementing-row-level-security-rls-in-postgresql","2. Implementing Row-Level Security (RLS) in PostgreSQL",[11,16758,16759,16760,16763],{},"If you choose ",[26,16761,16762],{},"Pattern C"," (Shared Database & Tables) due to its cost efficiency and ease of maintenance, you must guarantee that users cannot accidentally or maliciously access another customer's data.",[11,16765,16766,16767,16770],{},"PostgreSQL ",[26,16768,16769],{},"Row-Level Security (RLS)"," handles this by applying filter policies directly inside the SQL engine.",[70,16772,16774],{"id":16773},"step-1-create-tables-and-enable-rls","Step 1: Create Tables and Enable RLS",[11,16776,16777,16778,16781],{},"We will design a simple CRM table for storing leads. We explicitly enable RLS using the ",[49,16779,16780],{},"ALTER TABLE"," statement.",[1564,16783,16785],{"className":2100,"code":16784,"language":2102,"meta":363,"style":363},"-- Create tenants table\nCREATE TABLE tenants (\n  id SERIAL PRIMARY KEY,\n  company_name VARCHAR(255) NOT NULL,\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Create projects table with tenant mapping\nCREATE TABLE projects (\n  id SERIAL PRIMARY KEY,\n  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,\n  name VARCHAR(255) NOT NULL,\n  status VARCHAR(50) DEFAULT 'draft',\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Index the tenant_id column (Crucial for query performance across multiple tenants)\nCREATE INDEX idx_projects_tenant ON projects(tenant_id);\n\n-- Enable RLS for the projects table\nALTER TABLE projects ENABLE ROW LEVEL SECURITY;\n",[49,16786,16787,16792,16797,16802,16807,16812,16816,16820,16825,16830,16834,16839,16844,16849,16853,16857,16861,16866,16871,16875,16880],{"__ignoreMap":363},[1572,16788,16789],{"class":1574,"line":1575},[1572,16790,16791],{},"-- Create tenants table\n",[1572,16793,16794],{"class":1574,"line":364},[1572,16795,16796],{},"CREATE TABLE tenants (\n",[1572,16798,16799],{"class":1574,"line":370},[1572,16800,16801],{},"  id SERIAL PRIMARY KEY,\n",[1572,16803,16804],{"class":1574,"line":1591},[1572,16805,16806],{},"  company_name VARCHAR(255) NOT NULL,\n",[1572,16808,16809],{"class":1574,"line":1270},[1572,16810,16811],{},"  created_at TIMESTAMP DEFAULT NOW()\n",[1572,16813,16814],{"class":1574,"line":4389},[1572,16815,11784],{},[1572,16817,16818],{"class":1574,"line":742},[1572,16819,10309],{"emptyLinePlaceholder":396},[1572,16821,16822],{"class":1574,"line":10336},[1572,16823,16824],{},"-- Create projects table with tenant mapping\n",[1572,16826,16827],{"class":1574,"line":10342},[1572,16828,16829],{},"CREATE TABLE projects (\n",[1572,16831,16832],{"class":1574,"line":10348},[1572,16833,16801],{},[1572,16835,16836],{"class":1574,"line":11656},[1572,16837,16838],{},"  tenant_id INT NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,\n",[1572,16840,16841],{"class":1574,"line":11661},[1572,16842,16843],{},"  name VARCHAR(255) NOT NULL,\n",[1572,16845,16846],{"class":1574,"line":11671},[1572,16847,16848],{},"  status VARCHAR(50) DEFAULT 'draft',\n",[1572,16850,16851],{"class":1574,"line":11683},[1572,16852,16811],{},[1572,16854,16855],{"class":1574,"line":11695},[1572,16856,11784],{},[1572,16858,16859],{"class":1574,"line":11700},[1572,16860,10309],{"emptyLinePlaceholder":396},[1572,16862,16863],{"class":1574,"line":11705},[1572,16864,16865],{},"-- Index the tenant_id column (Crucial for query performance across multiple tenants)\n",[1572,16867,16868],{"class":1574,"line":11742},[1572,16869,16870],{},"CREATE INDEX idx_projects_tenant ON projects(tenant_id);\n",[1572,16872,16873],{"class":1574,"line":11763},[1572,16874,10309],{"emptyLinePlaceholder":396},[1572,16876,16877],{"class":1574,"line":11787},[1572,16878,16879],{},"-- Enable RLS for the projects table\n",[1572,16881,16882],{"class":1574,"line":11793},[1572,16883,16884],{},"ALTER TABLE projects ENABLE ROW LEVEL SECURITY;\n",[70,16886,16888],{"id":16887},"step-2-define-the-security-policy","Step 2: Define the Security Policy",[11,16890,16891,16892,507],{},"We will configure PostgreSQL to restrict access based on a session configuration variable called ",[49,16893,16894],{},"app.current_tenant_id",[1564,16896,16898],{"className":2100,"code":16897,"language":2102,"meta":363,"style":363},"CREATE POLICY tenant_isolation_policy ON projects\n  AS ASYMMETRIC\n  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::integer);\n",[49,16899,16900,16905,16910],{"__ignoreMap":363},[1572,16901,16902],{"class":1574,"line":1575},[1572,16903,16904],{},"CREATE POLICY tenant_isolation_policy ON projects\n",[1572,16906,16907],{"class":1574,"line":364},[1572,16908,16909],{},"  AS ASYMMETRIC\n",[1572,16911,16912],{"class":1574,"line":370},[1572,16913,16914],{},"  USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::integer);\n",[70,16916,16918],{"id":16917},"step-3-accessing-data-from-the-application-layer","Step 3: Accessing Data from the Application Layer",[11,16920,16921],{},"When your backend (e.g., Node.js\u002FFastify or Nuxt server routes) connects to PostgreSQL, it runs queries inside a transaction, setting the session context variable first.",[1564,16923,16925],{"className":11542,"code":16924,"language":11544,"meta":363,"style":363},"import { Client } from 'pg';\n\nasync function fetchTenantProjects(tenantId: number): Promise\u003Cany[]> {\n  const client = new Client({ connectionString: process.env.DATABASE_URL });\n  await client.connect();\n\n  try {\n    \u002F\u002F Start transaction\n    await client.query('BEGIN');\n\n    \u002F\u002F Set the tenant ID context variable for the duration of this transaction\n    await client.query(`SET LOCAL app.current_tenant_id = $1`, [tenantId]);\n\n    \u002F\u002F Even if we query SELECT * without a WHERE clause, RLS filters the records\n    const result = await client.query('SELECT * FROM projects');\n\n    await client.query('COMMIT');\n    return result.rows;\n  } catch (error) {\n    await client.query('ROLLBACK');\n    throw error;\n  } finally {\n    await client.end();\n  }\n}\n",[49,16926,16927,16941,16945,16975,16997,17009,17013,17019,17024,17040,17044,17049,17065,17069,17074,17096,17100,17115,17122,17130,17145,17152,17161,17172,17176],{"__ignoreMap":363},[1572,16928,16929,16931,16934,16936,16939],{"class":1574,"line":1575},[1572,16930,5892],{"class":11551},[1572,16932,16933],{"class":1638}," { Client } ",[1572,16935,11557],{"class":11551},[1572,16937,16938],{"class":1651}," 'pg'",[1572,16940,11563],{"class":1638},[1572,16942,16943],{"class":1574,"line":364},[1572,16944,10309],{"emptyLinePlaceholder":396},[1572,16946,16947,16949,16951,16954,16956,16958,16960,16962,16964,16966,16968,16970,16972],{"class":1574,"line":370},[1572,16948,16020],{"class":11551},[1572,16950,7934],{"class":11551},[1572,16952,16953],{"class":6311}," fetchTenantProjects",[1572,16955,11715],{"class":1638},[1572,16957,16237],{"class":11583},[1572,16959,8342],{"class":11551},[1572,16961,11589],{"class":1644},[1572,16963,219],{"class":1638},[1572,16965,8342],{"class":11551},[1572,16967,14603],{"class":6311},[1572,16969,6305],{"class":1638},[1572,16971,12437],{"class":1644},[1572,16973,16974],{"class":1638},"[]> {\n",[1572,16976,16977,16979,16982,16984,16986,16989,16992,16995],{"class":1574,"line":1591},[1572,16978,11745],{"class":11551},[1572,16980,16981],{"class":1644}," client",[1572,16983,11751],{"class":11551},[1572,16985,11754],{"class":11551},[1572,16987,16988],{"class":6311}," Client",[1572,16990,16991],{"class":1638},"({ connectionString: process.env.",[1572,16993,16994],{"class":1644},"DATABASE_URL",[1572,16996,15938],{"class":1638},[1572,16998,16999,17001,17004,17007],{"class":1574,"line":1270},[1572,17000,16176],{"class":11551},[1572,17002,17003],{"class":1638}," client.",[1572,17005,17006],{"class":6311},"connect",[1572,17008,11887],{"class":1638},[1572,17010,17011],{"class":1574,"line":4389},[1572,17012,10309],{"emptyLinePlaceholder":396},[1572,17014,17015,17017],{"class":1574,"line":742},[1572,17016,12185],{"class":11551},[1572,17018,11578],{"class":1638},[1572,17020,17021],{"class":1574,"line":10336},[1572,17022,17023],{"class":11834},"    \u002F\u002F Start transaction\n",[1572,17025,17026,17029,17031,17033,17035,17038],{"class":1574,"line":10342},[1572,17027,17028],{"class":11551},"    await",[1572,17030,17003],{"class":1638},[1572,17032,16182],{"class":6311},[1572,17034,11715],{"class":1638},[1572,17036,17037],{"class":1651},"'BEGIN'",[1572,17039,11784],{"class":1638},[1572,17041,17042],{"class":1574,"line":10348},[1572,17043,10309],{"emptyLinePlaceholder":396},[1572,17045,17046],{"class":1574,"line":11656},[1572,17047,17048],{"class":11834},"    \u002F\u002F Set the tenant ID context variable for the duration of this transaction\n",[1572,17050,17051,17053,17055,17057,17059,17062],{"class":1574,"line":11661},[1572,17052,17028],{"class":11551},[1572,17054,17003],{"class":1638},[1572,17056,16182],{"class":6311},[1572,17058,11715],{"class":1638},[1572,17060,17061],{"class":1651},"`SET LOCAL app.current_tenant_id = $1`",[1572,17063,17064],{"class":1638},", [tenantId]);\n",[1572,17066,17067],{"class":1574,"line":11671},[1572,17068,10309],{"emptyLinePlaceholder":396},[1572,17070,17071],{"class":1574,"line":11683},[1572,17072,17073],{"class":11834},"    \u002F\u002F Even if we query SELECT * without a WHERE clause, RLS filters the records\n",[1572,17075,17076,17078,17081,17083,17085,17087,17089,17091,17094],{"class":1574,"line":11695},[1572,17077,12193],{"class":11551},[1572,17079,17080],{"class":1644}," result",[1572,17082,11751],{"class":11551},[1572,17084,14623],{"class":11551},[1572,17086,17003],{"class":1638},[1572,17088,16182],{"class":6311},[1572,17090,11715],{"class":1638},[1572,17092,17093],{"class":1651},"'SELECT * FROM projects'",[1572,17095,11784],{"class":1638},[1572,17097,17098],{"class":1574,"line":11700},[1572,17099,10309],{"emptyLinePlaceholder":396},[1572,17101,17102,17104,17106,17108,17110,17113],{"class":1574,"line":11705},[1572,17103,17028],{"class":11551},[1572,17105,17003],{"class":1638},[1572,17107,16182],{"class":6311},[1572,17109,11715],{"class":1638},[1572,17111,17112],{"class":1651},"'COMMIT'",[1572,17114,11784],{"class":1638},[1572,17116,17117,17119],{"class":1574,"line":11742},[1572,17118,11809],{"class":11551},[1572,17120,17121],{"class":1638}," result.rows;\n",[1572,17123,17124,17126,17128],{"class":1574,"line":11763},[1572,17125,12264],{"class":1638},[1572,17127,12267],{"class":11551},[1572,17129,12270],{"class":1638},[1572,17131,17132,17134,17136,17138,17140,17143],{"class":1574,"line":11787},[1572,17133,17028],{"class":11551},[1572,17135,17003],{"class":1638},[1572,17137,16182],{"class":6311},[1572,17139,11715],{"class":1638},[1572,17141,17142],{"class":1651},"'ROLLBACK'",[1572,17144,11784],{"class":1638},[1572,17146,17147,17149],{"class":1574,"line":11793},[1572,17148,14776],{"class":11551},[1572,17150,17151],{"class":1638}," error;\n",[1572,17153,17154,17156,17159],{"class":1574,"line":11806},[1572,17155,12264],{"class":1638},[1572,17157,17158],{"class":11551},"finally",[1572,17160,11578],{"class":1638},[1572,17162,17163,17165,17167,17170],{"class":1574,"line":11820},[1572,17164,17028],{"class":11551},[1572,17166,17003],{"class":1638},[1572,17168,17169],{"class":6311},"end",[1572,17171,11887],{"class":1638},[1572,17173,17174],{"class":1574,"line":11826},[1572,17175,11823],{"class":1638},[1572,17177,17178],{"class":1574,"line":11831},[1572,17179,1670],{"class":1638},[15,17181,17183],{"id":17182},"_3-best-practices-for-scaling-multi-tenant-databases","3. Best Practices for Scaling Multi-Tenant Databases",[4001,17185,17186,17195,17201],{},[301,17187,17188,17194],{},[26,17189,17190,17191],{},"Always Index ",[49,17192,17193],{},"tenant_id",": If you don't index the tenant column, every query will perform a sequential table scan, destroying database performance as the table grows.",[301,17196,17197,17200],{},[26,17198,17199],{},"Verify policies in automated tests",": Write integration tests that attempt to fetch Tenant B's data using Tenant A's connection context, verifying that the database throws access errors or returns empty results.",[301,17202,17203,17206,17207,17209],{},[26,17204,17205],{},"Prepare for tenant migration",": Enterprise customers may eventually ask to move to a dedicated database (Pattern A) for regulatory reasons. Design your schemas so you can easily extract records matching a specific ",[49,17208,17193],{}," and load them into a standalone instance.",[11,17211,17212,17213,17215],{},"For example, in the ",[38,17214,13283],{"href":13282},", we successfully implemented Row-Level Isolation (Pattern C) using indexed tenant schemas, keeping monthly database fees extremely low while serving thousands of registered learners.",[15,17217,12723],{"id":12722},[298,17219,17220,17228,17236],{},[301,17221,17222,17227],{},[38,17223,17226],{"href":17224,"rel":17225},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-rowsecurity.html",[429],"PostgreSQL: Row Security Policies"," — official RLS documentation",[301,17229,17230,17235],{},[38,17231,17234],{"href":17232,"rel":17233},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fddl-schemas.html",[429],"PostgreSQL: Schemas"," — logical schemas for Pattern B",[301,17237,17238,17243],{},[38,17239,17242],{"href":17240,"rel":17241},"https:\u002F\u002Fgdpr.eu\u002F",[429],"GDPR.eu"," — isolation and data-portability requirements for personal data",[11,17245,17246,17247,507],{},"Choosing the right multi-tenant paradigm involves balancing your startup's budget, resources, and regulatory requirements. RLS offers an elegant and secure way to build a cost-effective, scalable B2B SaaS without compromising data safety. To see how this database setup ties into a full development lifecycle, explore my guide on ",[38,17248,17249],{"href":14211},"Building B2B SaaS MVPs in 30 Days",[11,17251,17252,17253,13330,17255,17257],{},"If you are designing the data layer for a multi-tenant application and want an expert architecture review to ensure it handles growth safely, explore my ",[38,17254,13451],{"href":13817},[38,17256,12756],{"href":12755}," to get a production plan.",[1779,17259,17260],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":363,"searchDepth":364,"depth":364,"links":17262},[17263,17264,17265,17270,17271],{"id":16594,"depth":364,"text":16595},{"id":16604,"depth":364,"text":16605},{"id":16755,"depth":364,"text":16756,"children":17266},[17267,17268,17269],{"id":16773,"depth":370,"text":16774},{"id":16887,"depth":370,"text":16888},{"id":16917,"depth":370,"text":16918},{"id":17182,"depth":364,"text":17183},{"id":12722,"depth":364,"text":12723},"2026-06-11","A comprehensive deep dive into designing secure, scalable multi-tenant database systems for B2B SaaS. We compare shared tables, PostgreSQL schemas, and independent databases.",[17275,17278,17281],{"q":17276,"a":17277},"Which multi-tenancy pattern should a SaaS start with?","For most B2B startups — a shared database with Row-Level Security (Pattern C): minimal server costs, simple migrations, and sufficient isolation when policies are written correctly. Dedicated databases are justified only by strict regulatory requirements.",{"q":17279,"a":17280},"How safe is isolation via PostgreSQL RLS?","With RLS policies enabled, the database itself filters rows by tenant_id at the engine level — even a buggy application query won't return another tenant's data. The mandatory companion: integration tests that try to read one tenant's data through another tenant's session.",{"q":17282,"a":17283},"What if a large customer demands a dedicated database?","Design for export upfront: if every table is linked through tenant_id, one customer's data can be extracted with standard filtering and moved to a standalone instance without rewriting the application.","\u002Fimages\u002Fblog\u002Fblog_saas_multitenancy.webp",{},"\u002Fblog\u002Fen\u002Fsaas-multitenancy-databases",{"title":16583,"description":17273},"blog\u002Fen\u002Fsaas-multitenancy-databases",[12792,17290,14918,17291,17292],"Databases","PostgreSQL","SQL","d67AmmvKaNREjfq8de34gJhZmQ7ZhvGYUZWr7lUifD8",{"id":17295,"title":17296,"body":17297,"date":17955,"description":17956,"extension":376,"faq":17957,"image":17967,"lang":394,"meta":17968,"navigation":396,"path":17969,"published":396,"readTime":370,"seo":17970,"stem":17971,"tags":17972,"updated":12772,"__hash__":17976},"blog\u002Fblog\u002Fen\u002Fcode-audit-technical-debt.md","Codebase Audit Checklist: How to Assess Technical Debt Before Scaling",{"type":8,"value":17298,"toc":17945},[17299,17302,17305,17311,17314,17318,17321,17408,17412,17416,17419,17424,17587,17591,17594,17694,17698,17701,17726,17845,17849,17852,17878,17882,17885,17905,17907,17930,17935,17942],[11,17300,17301],{},"Taking over a legacy codebase, acquiring an existing SaaS startup, or preparing a MVP for a major scaling phase is a critical turning point. In these scenarios, technical debt is your biggest invisible enemy.",[11,17303,17304],{},"Code that appears to function perfectly on a developer's machine can harbor security vulnerabilities, lack proper database indices, or contain architectural flaws that make adding new features take weeks instead of days.",[11,17306,17307,17308,507],{},"To scale safely and prevent expensive rewrite cycles, you must perform a thorough ",[26,17309,17310],{},"codebase audit",[11,17312,17313],{},"In this senior developer guide, we present a technical playbook for auditing Node.js\u002FTypeScript applications, identifying DB bottlenecks (like the N+1 query problem), checking security controls, and organizing a refactoring plan.",[15,17315,17317],{"id":17316},"_1-automated-meticulous-metrics-check","1. Automated Meticulous Metrics Check",[11,17319,17320],{},"Before looking at custom business logic, gather quantitative metrics using specialized analysis tooling. This sets a baseline for your engineering teams:",[75,17322,17323,17339],{},[78,17324,17325],{},[81,17326,17327,17330,17333,17336],{},[84,17328,17329],{"align":12854},"Audit Layer",[84,17331,17332],{"align":12854},"Tool Recommendation",[84,17334,17335],{"align":12854},"Standard Metric",[84,17337,17338],{"align":12854},"Impact of Failure",[91,17340,17341,17360,17376,17392],{},[81,17342,17343,17348,17354,17357],{},[96,17344,17345],{"align":12854},[26,17346,17347],{},"Dependency Security",[96,17349,17350,17353],{"align":12854},[49,17351,17352],{},"npm audit",", Snyk, Dependabot",[96,17355,17356],{"align":12854},"0 Critical\u002FHigh issues",[96,17358,17359],{"align":12854},"Remote Code Execution, data injection vulnerabilities.",[81,17361,17362,17367,17370,17373],{},[96,17363,17364],{"align":12854},[26,17365,17366],{},"Code Style & Linting",[96,17368,17369],{"align":12854},"Biome, ESLint, Prettier",[96,17371,17372],{"align":12854},"0 warnings\u002Ferrors",[96,17374,17375],{"align":12854},"Visual inconsistency, syntax pitfalls, merge conflicts.",[81,17377,17378,17383,17386,17389],{},[96,17379,17380],{"align":12854},[26,17381,17382],{},"Test Coverage",[96,17384,17385],{"align":12854},"Vitest, Jest, Playwright",[96,17387,17388],{"align":12854},"> 70% unit coverage",[96,17390,17391],{"align":12854},"High regression risk when refactoring core modules.",[81,17393,17394,17399,17402,17405],{},[96,17395,17396],{"align":12854},[26,17397,17398],{},"Complexity Analysis",[96,17400,17401],{"align":12854},"SonarQube, Plato",[96,17403,17404],{"align":12854},"Low Cyclomatic Complexity",[96,17406,17407],{"align":12854},"Hard-to-read functions with deep nested conditional branches.",[15,17409,17411],{"id":17410},"_2-technical-audit-crucial-checkpoints","2. Technical Audit: Crucial Checkpoints",[70,17413,17415],{"id":17414},"a-the-database-layer-solving-the-n1-query-problem","A. The Database Layer: Solving the N+1 Query Problem",[11,17417,17418],{},"A common scaling bottleneck is the N+1 database query pattern. This occurs when your backend queries a database to retrieve a list of records, and then runs separate queries for each retrieved record to fetch associated relationships.",[17420,17421,17423],"h4",{"id":17422},"the-bad-pattern-n1-query-in-express-api","The Bad Pattern (N+1 Query in Express API)",[1564,17425,17427],{"className":11542,"code":17426,"language":11544,"meta":363,"style":363},"\u002F\u002F GET \u002Fapi\u002Fprojects - Pulls N projects, then makes N database calls to get users\napp.get('\u002Fapi\u002Fprojects', async (req, res) => {\n  const projects = await db.query('SELECT * FROM projects'); \u002F\u002F 1 Query\n  const enrichedProjects = [];\n\n  for (const project of projects) {\n    \u002F\u002F RUNS N TIMES! If you have 1000 projects, this runs 1000 queries.\n    const user = await db.query('SELECT * FROM users WHERE id = $1', [project.userId]); \n    enrichedProjects.push({ ...project, user });\n  }\n\n  res.json(enrichedProjects);\n});\n",[49,17428,17429,17434,17463,17488,17500,17504,17522,17527,17549,17565,17569,17573,17583],{"__ignoreMap":363},[1572,17430,17431],{"class":1574,"line":1575},[1572,17432,17433],{"class":11834},"\u002F\u002F GET \u002Fapi\u002Fprojects - Pulls N projects, then makes N database calls to get users\n",[1572,17435,17436,17438,17440,17442,17445,17447,17449,17451,17453,17455,17457,17459,17461],{"class":1574,"line":364},[1572,17437,15431],{"class":1638},[1572,17439,11776],{"class":6311},[1572,17441,11715],{"class":1638},[1572,17443,17444],{"class":1651},"'\u002Fapi\u002Fprojects'",[1572,17446,673],{"class":1638},[1572,17448,16020],{"class":11551},[1572,17450,4110],{"class":1638},[1572,17452,15471],{"class":11583},[1572,17454,673],{"class":1638},[1572,17456,15485],{"class":11583},[1572,17458,15497],{"class":1638},[1572,17460,12459],{"class":11551},[1572,17462,11578],{"class":1638},[1572,17464,17465,17467,17470,17472,17474,17476,17478,17480,17482,17485],{"class":1574,"line":370},[1572,17466,11745],{"class":11551},[1572,17468,17469],{"class":1644}," projects",[1572,17471,11751],{"class":11551},[1572,17473,14623],{"class":11551},[1572,17475,16179],{"class":1638},[1572,17477,16182],{"class":6311},[1572,17479,11715],{"class":1638},[1572,17481,17093],{"class":1651},[1572,17483,17484],{"class":1638},"); ",[1572,17486,17487],{"class":11834},"\u002F\u002F 1 Query\n",[1572,17489,17490,17492,17495,17497],{"class":1574,"line":1591},[1572,17491,11745],{"class":11551},[1572,17493,17494],{"class":1644}," enrichedProjects",[1572,17496,11751],{"class":11551},[1572,17498,17499],{"class":1638}," [];\n",[1572,17501,17502],{"class":1574,"line":1270},[1572,17503,10309],{"emptyLinePlaceholder":396},[1572,17505,17506,17509,17511,17513,17516,17519],{"class":1574,"line":4389},[1572,17507,17508],{"class":11551},"  for",[1572,17510,4110],{"class":1638},[1572,17512,5852],{"class":11551},[1572,17514,17515],{"class":1644}," project",[1572,17517,17518],{"class":11551}," of",[1572,17520,17521],{"class":1638}," projects) {\n",[1572,17523,17524],{"class":1574,"line":742},[1572,17525,17526],{"class":11834},"    \u002F\u002F RUNS N TIMES! If you have 1000 projects, this runs 1000 queries.\n",[1572,17528,17529,17531,17533,17535,17537,17539,17541,17543,17546],{"class":1574,"line":10336},[1572,17530,12193],{"class":11551},[1572,17532,12217],{"class":1644},[1572,17534,11751],{"class":11551},[1572,17536,14623],{"class":11551},[1572,17538,16179],{"class":1638},[1572,17540,16182],{"class":6311},[1572,17542,11715],{"class":1638},[1572,17544,17545],{"class":1651},"'SELECT * FROM users WHERE id = $1'",[1572,17547,17548],{"class":1638},", [project.userId]); \n",[1572,17550,17551,17554,17556,17559,17562],{"class":1574,"line":10342},[1572,17552,17553],{"class":1638},"    enrichedProjects.",[1572,17555,8107],{"class":6311},[1572,17557,17558],{"class":1638},"({ ",[1572,17560,17561],{"class":11551},"...",[1572,17563,17564],{"class":1638},"project, user });\n",[1572,17566,17567],{"class":1574,"line":10348},[1572,17568,11823],{"class":1638},[1572,17570,17571],{"class":1574,"line":11656},[1572,17572,10309],{"emptyLinePlaceholder":396},[1572,17574,17575,17578,17580],{"class":1574,"line":11661},[1572,17576,17577],{"class":1638},"  res.",[1572,17579,1631],{"class":6311},[1572,17581,17582],{"class":1638},"(enrichedProjects);\n",[1572,17584,17585],{"class":1574,"line":11671},[1572,17586,13911],{"class":1638},[17420,17588,17590],{"id":17589},"the-optimized-pattern-sql-joins","The Optimized Pattern (SQL Joins)",[11,17592,17593],{},"Combine these into a single database execution:",[1564,17595,17597],{"className":11542,"code":17596,"language":11544,"meta":363,"style":363},"app.get('\u002Fapi\u002Fprojects', async (req, res) => {\n  const query = `\n    SELECT p.*, row_to_json(u.*) as user \n    FROM projects p\n    LEFT JOIN users u ON p.userId = u.id\n  `;\n  const result = await db.query(query); \u002F\u002F 1 Single Query instead of N+1\n  res.json(result.rows);\n});\n",[49,17598,17599,17627,17639,17644,17649,17654,17661,17681,17690],{"__ignoreMap":363},[1572,17600,17601,17603,17605,17607,17609,17611,17613,17615,17617,17619,17621,17623,17625],{"class":1574,"line":1575},[1572,17602,15431],{"class":1638},[1572,17604,11776],{"class":6311},[1572,17606,11715],{"class":1638},[1572,17608,17444],{"class":1651},[1572,17610,673],{"class":1638},[1572,17612,16020],{"class":11551},[1572,17614,4110],{"class":1638},[1572,17616,15471],{"class":11583},[1572,17618,673],{"class":1638},[1572,17620,15485],{"class":11583},[1572,17622,15497],{"class":1638},[1572,17624,12459],{"class":11551},[1572,17626,11578],{"class":1638},[1572,17628,17629,17631,17634,17636],{"class":1574,"line":364},[1572,17630,11745],{"class":11551},[1572,17632,17633],{"class":1644}," query",[1572,17635,11751],{"class":11551},[1572,17637,17638],{"class":1651}," `\n",[1572,17640,17641],{"class":1574,"line":370},[1572,17642,17643],{"class":1651},"    SELECT p.*, row_to_json(u.*) as user \n",[1572,17645,17646],{"class":1574,"line":1591},[1572,17647,17648],{"class":1651},"    FROM projects p\n",[1572,17650,17651],{"class":1574,"line":1270},[1572,17652,17653],{"class":1651},"    LEFT JOIN users u ON p.userId = u.id\n",[1572,17655,17656,17659],{"class":1574,"line":4389},[1572,17657,17658],{"class":1651},"  `",[1572,17660,11563],{"class":1638},[1572,17662,17663,17665,17667,17669,17671,17673,17675,17678],{"class":1574,"line":742},[1572,17664,11745],{"class":11551},[1572,17666,17080],{"class":1644},[1572,17668,11751],{"class":11551},[1572,17670,14623],{"class":11551},[1572,17672,16179],{"class":1638},[1572,17674,16182],{"class":6311},[1572,17676,17677],{"class":1638},"(query); ",[1572,17679,17680],{"class":11834},"\u002F\u002F 1 Single Query instead of N+1\n",[1572,17682,17683,17685,17687],{"class":1574,"line":10336},[1572,17684,17577],{"class":1638},[1572,17686,1631],{"class":6311},[1572,17688,17689],{"class":1638},"(result.rows);\n",[1572,17691,17692],{"class":1574,"line":10342},[1572,17693,13911],{"class":1638},[70,17695,17697],{"id":17696},"b-secret-management-and-environment-parity","B. Secret Management and Environment Parity",[11,17699,17700],{},"Check where API tokens, private keys, and passwords are stored.",[298,17702,17703,17716],{},[301,17704,17705,17708,17709,17711,17712,17715],{},[26,17706,17707],{},"Zero Hardcoding",": Ensure no ",[49,17710,3786],{}," files or raw strings are committed to Git. Use ",[49,17713,17714],{},"git-secrets"," or Snyk to scan Git history for leaked credentials.",[301,17717,17718,17721,17722,17725],{},[26,17719,17720],{},"Configuration Validation",": Use a library like ",[49,17723,17724],{},"zod"," to validate that all required environment variables are set at startup:",[1564,17727,17729],{"className":11542,"code":17728,"language":11544,"meta":363,"style":363},"import { z } from 'zod';\n\nconst envSchema = z.object({\n  DATABASE_URL: z.string().url(),\n  PORT: z.string().transform(Number),\n  STRIPE_SECRET_KEY: z.string().min(10),\n});\n\nexport const ENV = envSchema.parse(process.env);\n",[49,17730,17731,17745,17749,17766,17782,17797,17817,17821,17825],{"__ignoreMap":363},[1572,17732,17733,17735,17738,17740,17743],{"class":1574,"line":1575},[1572,17734,5892],{"class":11551},[1572,17736,17737],{"class":1638}," { z } ",[1572,17739,11557],{"class":11551},[1572,17741,17742],{"class":1651}," 'zod'",[1572,17744,11563],{"class":1638},[1572,17746,17747],{"class":1574,"line":364},[1572,17748,10309],{"emptyLinePlaceholder":396},[1572,17750,17751,17753,17756,17758,17761,17764],{"class":1574,"line":370},[1572,17752,5852],{"class":11551},[1572,17754,17755],{"class":1644}," envSchema",[1572,17757,11751],{"class":11551},[1572,17759,17760],{"class":1638}," z.",[1572,17762,17763],{"class":6311},"object",[1572,17765,12326],{"class":1638},[1572,17767,17768,17771,17773,17776,17779],{"class":1574,"line":1591},[1572,17769,17770],{"class":1638},"  DATABASE_URL: z.",[1572,17772,14608],{"class":6311},[1572,17774,17775],{"class":1638},"().",[1572,17777,17778],{"class":6311},"url",[1572,17780,17781],{"class":1638},"(),\n",[1572,17783,17784,17787,17789,17791,17794],{"class":1574,"line":1270},[1572,17785,17786],{"class":1638},"  PORT: z.",[1572,17788,14608],{"class":6311},[1572,17790,17775],{"class":1638},[1572,17792,17793],{"class":6311},"transform",[1572,17795,17796],{"class":1638},"(Number),\n",[1572,17798,17799,17802,17804,17806,17809,17811,17814],{"class":1574,"line":4389},[1572,17800,17801],{"class":1638},"  STRIPE_SECRET_KEY: z.",[1572,17803,14608],{"class":6311},[1572,17805,17775],{"class":1638},[1572,17807,17808],{"class":6311},"min",[1572,17810,11715],{"class":1638},[1572,17812,17813],{"class":1644},"10",[1572,17815,17816],{"class":1638},"),\n",[1572,17818,17819],{"class":1574,"line":742},[1572,17820,13911],{"class":1638},[1572,17822,17823],{"class":1574,"line":10336},[1572,17824,10309],{"emptyLinePlaceholder":396},[1572,17826,17827,17829,17832,17835,17837,17840,17842],{"class":1574,"line":10342},[1572,17828,3790],{"class":11551},[1572,17830,17831],{"class":11551}," const",[1572,17833,17834],{"class":1644}," ENV",[1572,17836,11751],{"class":11551},[1572,17838,17839],{"class":1638}," envSchema.",[1572,17841,12236],{"class":6311},[1572,17843,17844],{"class":1638},"(process.env);\n",[70,17846,17848],{"id":17847},"c-security-and-input-sanitization","C. Security and Input Sanitization",[11,17850,17851],{},"Inspect HTTP route entrypoints.",[298,17853,17854,17868],{},[301,17855,17856,17859,17860,17863,17864,17867],{},[26,17857,17858],{},"SQL Injection Prevention",": Ensure the codebase never concatenates strings directly into SQL statements (e.g., ",[49,17861,17862],{},"db.query(\"SELECT * FROM users WHERE id = \" + userId)","). Always utilize parameterized queries (",[49,17865,17866],{},"db.query('SELECT * FROM users WHERE id = $1', [userId])",") or verified ORMs.",[301,17869,17870,17873,17874,17877],{},[26,17871,17872],{},"HTTP Security Headers",": Verify that the application uses the ",[49,17875,17876],{},"helmet"," package to automatically inject security headers (CSP, HSTS, X-Frame-Options) to protect against cross-site scripting (XSS) and clickjacking.",[15,17879,17881],{"id":17880},"_3-organizing-the-refactoring-plan","3. Organizing the Refactoring Plan",[11,17883,17884],{},"After completing the codebase audit, document the technical debt and prioritize fixes into three buckets:",[4001,17886,17887,17893,17899],{},[301,17888,17889,17892],{},[26,17890,17891],{},"Blocker Issues (P0)",": Security risks, hardcoded API secrets, lack of backups, or broken authentication layers. Fix immediately before writing new features.",[301,17894,17895,17898],{},[26,17896,17897],{},"Performance Bottlenecks (P1)",": Missing indexes on primary DB keys, N+1 queries, or slow third-party API calls lacking cache layers. Plan to refactor in the next sprint.",[301,17900,17901,17904],{},[26,17902,17903],{},"Technical Debt Cleanup (P2)",": Formatting errors, lack of comments, code duplication, and low test coverage. Allocate 20% of every development sprint to address these.",[15,17906,12723],{"id":12722},[298,17908,17909,17915,17922],{},[301,17910,17911,17914],{},[38,17912,10613],{"href":12988,"rel":17913},[429]," — the reference list of web vulnerabilities for the security block",[301,17916,17917,17921],{},[38,17918,17352],{"href":17919,"rel":17920},"https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fcommands\u002Fnpm-audit",[429]," — checking dependencies for known vulnerabilities",[301,17923,17924,17929],{},[38,17925,17928],{"href":17926,"rel":17927},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Findexes.html",[429],"PostgreSQL: Indexes"," — the official indexing guide",[11,17931,17932,17933,507],{},"Regular technical codebase audits are key to maintaining developer velocity, reducing server overhead, and ensuring your SaaS scales seamlessly. Performing these audits is especially important if you suspect your code was rushed by budget agencies; read my analysis on ",[38,17934,14923],{"href":14204},[11,17936,17937,17938,17941],{},"If you are taking over a project and need a professional third-party code audit, an architectural review, or want to establish code quality rules for your engineering team, visit my ",[38,17939,17940],{"href":12755},"Technical Consultations Service"," page or book a session.",[1779,17943,17944],{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":17946},[17947,17948,17953,17954],{"id":17316,"depth":364,"text":17317},{"id":17410,"depth":364,"text":17411,"children":17949},[17950,17951,17952],{"id":17414,"depth":370,"text":17415},{"id":17696,"depth":370,"text":17697},{"id":17847,"depth":370,"text":17848},{"id":17880,"depth":364,"text":17881},{"id":12722,"depth":364,"text":12723},"2026-06-07","A practical senior developer playbook for auditing legacy Node.js\u002FTypeScript codebases, verifying security, mapping DB indexes, and identifying N+1 query patterns.",[17958,17961,17964],{"q":17959,"a":17960},"When does a codebase need a technical audit?","At three critical points: taking over a legacy project from another team, acquiring an existing product, and right before an active scaling phase. An audit in week 2–3 of a new project is cheaper than any audit after release.",{"q":17962,"a":17963},"What gets checked first?","Automated metrics first (dependency vulnerabilities via npm audit, linting, test coverage), then manual points: parameterized SQL queries, indexes on foreign keys, N+1 patterns, secrets in the repository, and error handling.",{"q":17965,"a":17966},"What do I do with the findings?","Split them by priority: P0 — security and backups, fix immediately; P1 — performance (indexes, N+1, caching), next sprint; P2 — code cleanup, a steady 20% of every sprint.","\u002Fimages\u002Fblog\u002Fblog_code_audit.webp",{},"\u002Fblog\u002Fen\u002Fcode-audit-technical-debt",{"title":17296,"description":17956},"blog\u002Fen\u002Fcode-audit-technical-debt",[14918,17973,17974,15220,17975],"CodeReview","Security","TechDebt","akZOyjz6i62NGIdPKesrorYjCCJiIGDJb6E4EwCs1aM",{"id":17978,"title":17979,"body":17980,"date":19378,"description":19379,"extension":376,"faq":19380,"image":19390,"lang":394,"meta":19391,"navigation":396,"path":19392,"published":396,"readTime":1591,"seo":19393,"stem":19394,"tags":19395,"updated":12772,"__hash__":19398},"blog\u002Fblog\u002Fen\u002Ftelegram-bot-ai-rag-support.md","AI-Powered Customer Support: Building a RAG Bot with Live Agent Handover",{"type":8,"value":17981,"toc":19370},[17982,17985,17996,18003,18010,18014,18020,18024,18034,18104,18107,18191,18195,18198,18606,18610,18613,18864,18868,18875,19313,19319,19330,19332,19357,19360,19367],[11,17983,17984],{},"Deploying large language models (LLMs) like ChatGPT to answer client support queries is a highly effective way to reduce operational costs. However, standard LLMs suffer from two major flaws in corporate environments: they have no access to your private company data (like API documentation, refund guidelines, or custom plans), and they occasionally hallucinate incorrect information.",[11,17986,17987,17988,17991,17992,17995],{},"To solve this, modern production architectures use the ",[26,17989,17990],{},"RAG (Retrieval-Augmented Generation)"," framework. RAG restricts the AI to search a local database first, extract relevant knowledge blocks, and answer the user query based ",[1321,17993,17994],{},"strictly"," on that retrieved text.",[11,17997,17998,17999,18002],{},"Furthermore, to maintain high brand standards, your support system must implement a ",[26,18000,18001],{},"live agent handover protocol"," to route the conversation to a human support manager the second the AI struggles to find an answer.",[11,18004,18005,18006,18009],{},"In this developer guide, we will build a complete RAG execution loop using Node.js, PostgreSQL ",[49,18007,18008],{},"pgvector",", and a hybrid user-to-manager routing machine.",[15,18011,18013],{"id":18012},"technical-rag-workflow","Technical RAG Workflow",[1564,18015,18018],{"className":18016,"code":18017,"language":4815,"meta":363},[4813],"[User asks: \"How to connect Stripe?\"]\n               │\n               v\n  (Generate Vector Embedding)\n  [text-embedding-3-small] (1536 dim)\n               │\n               v\n  (Vector Search in PostgreSQL)\n  SELECT * FROM match_kb_docs(vector, threshold = 0.75)\n               │\n               +----------------------+\n               |                      |\n      (Context Found)        (Context NOT Found \u002F Low Score)\n               │                      │\n               v                      v\n  [Assemble Prompt with Docs]   [Set Session = human_handling]\n               │                      │\n               v                      v\n  [Send to gpt-4o-mini]         [Notify Support Team in Slack]\n               │                      │\n               v                      v\n    [Send Answer to User]        [Alert user: \"Transferring...\"]\n",[49,18019,18017],{"__ignoreMap":363},[15,18021,18023],{"id":18022},"_1-setting-up-pgvector-database","1. Setting Up Pgvector Database",[11,18025,18026,18027,18030,18031,8399],{},"First, enable the ",[49,18028,18029],{},"vector"," extension in PostgreSQL and create a table to store documentation chunks and their corresponding embedding coordinates (dimensions must match the embedding model, e.g., 1536 dimensions for OpenAI's ",[49,18032,18033],{},"text-embedding-3-small",[1564,18035,18037],{"className":2100,"code":18036,"language":2102,"meta":363,"style":363},"-- Enable vector support extension\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- Create Knowledge Base table\nCREATE TABLE kb_documents (\n  id SERIAL PRIMARY KEY,\n  content TEXT NOT NULL,\n  embedding VECTOR(1536) NOT NULL,\n  category VARCHAR(50) DEFAULT 'general',\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Index vector column for fast search (HNSW index recommended for scalability)\nCREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n",[49,18038,18039,18044,18049,18053,18058,18063,18067,18072,18077,18082,18086,18090,18094,18099],{"__ignoreMap":363},[1572,18040,18041],{"class":1574,"line":1575},[1572,18042,18043],{},"-- Enable vector support extension\n",[1572,18045,18046],{"class":1574,"line":364},[1572,18047,18048],{},"CREATE EXTENSION IF NOT EXISTS vector;\n",[1572,18050,18051],{"class":1574,"line":370},[1572,18052,10309],{"emptyLinePlaceholder":396},[1572,18054,18055],{"class":1574,"line":1591},[1572,18056,18057],{},"-- Create Knowledge Base table\n",[1572,18059,18060],{"class":1574,"line":1270},[1572,18061,18062],{},"CREATE TABLE kb_documents (\n",[1572,18064,18065],{"class":1574,"line":4389},[1572,18066,16801],{},[1572,18068,18069],{"class":1574,"line":742},[1572,18070,18071],{},"  content TEXT NOT NULL,\n",[1572,18073,18074],{"class":1574,"line":10336},[1572,18075,18076],{},"  embedding VECTOR(1536) NOT NULL,\n",[1572,18078,18079],{"class":1574,"line":10342},[1572,18080,18081],{},"  category VARCHAR(50) DEFAULT 'general',\n",[1572,18083,18084],{"class":1574,"line":10348},[1572,18085,16811],{},[1572,18087,18088],{"class":1574,"line":11656},[1572,18089,11784],{},[1572,18091,18092],{"class":1574,"line":11661},[1572,18093,10309],{"emptyLinePlaceholder":396},[1572,18095,18096],{"class":1574,"line":11671},[1572,18097,18098],{},"-- Index vector column for fast search (HNSW index recommended for scalability)\n",[1572,18100,18101],{"class":1574,"line":11683},[1572,18102,18103],{},"CREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n",[11,18105,18106],{},"Next, create the SQL function to retrieve documents based on cosine similarity:",[1564,18108,18110],{"className":2100,"code":18109,"language":2102,"meta":363,"style":363},"CREATE OR REPLACE FUNCTION match_kb_docs(\n  query_embedding VECTOR(1536),\n  match_threshold FLOAT,\n  match_count INT\n)\nRETURNS TABLE (id INT, content TEXT, similarity FLOAT)\nLANGUAGE plpgsql AS $$\nBEGIN\n  RETURN QUERY\n  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n  FROM kb_documents kb\n  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n  ORDER BY kb.embedding \u003C=> query_embedding\n  LIMIT match_count;\nEND;\n$$;\n",[49,18111,18112,18117,18122,18127,18132,18136,18141,18146,18151,18156,18161,18166,18171,18176,18181,18186],{"__ignoreMap":363},[1572,18113,18114],{"class":1574,"line":1575},[1572,18115,18116],{},"CREATE OR REPLACE FUNCTION match_kb_docs(\n",[1572,18118,18119],{"class":1574,"line":364},[1572,18120,18121],{},"  query_embedding VECTOR(1536),\n",[1572,18123,18124],{"class":1574,"line":370},[1572,18125,18126],{},"  match_threshold FLOAT,\n",[1572,18128,18129],{"class":1574,"line":1591},[1572,18130,18131],{},"  match_count INT\n",[1572,18133,18134],{"class":1574,"line":1270},[1572,18135,11953],{},[1572,18137,18138],{"class":1574,"line":4389},[1572,18139,18140],{},"RETURNS TABLE (id INT, content TEXT, similarity FLOAT)\n",[1572,18142,18143],{"class":1574,"line":742},[1572,18144,18145],{},"LANGUAGE plpgsql AS $$\n",[1572,18147,18148],{"class":1574,"line":10336},[1572,18149,18150],{},"BEGIN\n",[1572,18152,18153],{"class":1574,"line":10342},[1572,18154,18155],{},"  RETURN QUERY\n",[1572,18157,18158],{"class":1574,"line":10348},[1572,18159,18160],{},"  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n",[1572,18162,18163],{"class":1574,"line":11656},[1572,18164,18165],{},"  FROM kb_documents kb\n",[1572,18167,18168],{"class":1574,"line":11661},[1572,18169,18170],{},"  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n",[1572,18172,18173],{"class":1574,"line":11671},[1572,18174,18175],{},"  ORDER BY kb.embedding \u003C=> query_embedding\n",[1572,18177,18178],{"class":1574,"line":11683},[1572,18179,18180],{},"  LIMIT match_count;\n",[1572,18182,18183],{"class":1574,"line":11695},[1572,18184,18185],{},"END;\n",[1572,18187,18188],{"class":1574,"line":11700},[1572,18189,18190],{},"$$;\n",[15,18192,18194],{"id":18193},"_2-implementing-rag-fetching-in-nodejs","2. Implementing RAG Fetching in Node.js",[11,18196,18197],{},"We write a service to generate embeddings for user questions via the OpenAI API and execute the vector search.",[1564,18199,18201],{"className":11542,"code":18200,"language":11544,"meta":363,"style":363},"import { OpenAI } from 'openai';\nimport pg from 'pg';\n\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\n\nexport async function getEmbedding(text: string): Promise\u003Cnumber[]> {\n  const response = await openai.embeddings.create({\n    model: 'text-embedding-3-small',\n    input: text.replace(\u002F\\n\u002Fg, ' '),\n  });\n  return response.data[0].embedding;\n}\n\nexport async function findRelevantContext(queryText: string): Promise\u003Cstring> {\n  const queryVector = await getEmbedding(queryText);\n  const sql = `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`;\n  \n  const client = await pool.connect();\n  try {\n    const result = await client.query(sql, [JSON.stringify(queryVector)]);\n    if (result.rows.length === 0) {\n      return '';\n    }\n    \u002F\u002F Combine top matches into a single text block\n    return result.rows.map(row => row.content).join('\\n---\\n');\n  } finally {\n    client.release();\n  }\n}\n",[49,18202,18203,18217,18230,18234,18256,18279,18283,18315,18333,18343,18369,18373,18385,18389,18393,18425,18441,18455,18459,18476,18482,18509,18527,18536,18540,18545,18580,18588,18598,18602],{"__ignoreMap":363},[1572,18204,18205,18207,18210,18212,18215],{"class":1574,"line":1575},[1572,18206,5892],{"class":11551},[1572,18208,18209],{"class":1638}," { OpenAI } ",[1572,18211,11557],{"class":11551},[1572,18213,18214],{"class":1651}," 'openai'",[1572,18216,11563],{"class":1638},[1572,18218,18219,18221,18224,18226,18228],{"class":1574,"line":364},[1572,18220,5892],{"class":11551},[1572,18222,18223],{"class":1638}," pg ",[1572,18225,11557],{"class":11551},[1572,18227,16938],{"class":1651},[1572,18229,11563],{"class":1638},[1572,18231,18232],{"class":1574,"line":370},[1572,18233,10309],{"emptyLinePlaceholder":396},[1572,18235,18236,18238,18241,18243,18245,18248,18251,18254],{"class":1574,"line":1591},[1572,18237,5852],{"class":11551},[1572,18239,18240],{"class":1644}," openai",[1572,18242,11751],{"class":11551},[1572,18244,11754],{"class":11551},[1572,18246,18247],{"class":6311}," OpenAI",[1572,18249,18250],{"class":1638},"({ apiKey: process.env.",[1572,18252,18253],{"class":1644},"OPENAI_API_KEY",[1572,18255,15938],{"class":1638},[1572,18257,18258,18260,18263,18265,18267,18270,18273,18275,18277],{"class":1574,"line":1270},[1572,18259,5852],{"class":11551},[1572,18261,18262],{"class":1644}," pool",[1572,18264,11751],{"class":11551},[1572,18266,11754],{"class":11551},[1572,18268,18269],{"class":1638}," pg.",[1572,18271,18272],{"class":6311},"Pool",[1572,18274,16991],{"class":1638},[1572,18276,16994],{"class":1644},[1572,18278,15938],{"class":1638},[1572,18280,18281],{"class":1574,"line":4389},[1572,18282,10309],{"emptyLinePlaceholder":396},[1572,18284,18285,18287,18289,18291,18294,18296,18298,18300,18302,18304,18306,18308,18310,18313],{"class":1574,"line":742},[1572,18286,3790],{"class":11551},[1572,18288,14583],{"class":11551},[1572,18290,7934],{"class":11551},[1572,18292,18293],{"class":6311}," getEmbedding",[1572,18295,11715],{"class":1638},[1572,18297,4815],{"class":11583},[1572,18299,8342],{"class":11551},[1572,18301,11601],{"class":1644},[1572,18303,219],{"class":1638},[1572,18305,8342],{"class":11551},[1572,18307,14603],{"class":6311},[1572,18309,6305],{"class":1638},[1572,18311,18312],{"class":1644},"number",[1572,18314,16974],{"class":1638},[1572,18316,18317,18319,18322,18324,18326,18329,18331],{"class":1574,"line":10336},[1572,18318,11745],{"class":11551},[1572,18320,18321],{"class":1644}," response",[1572,18323,11751],{"class":11551},[1572,18325,14623],{"class":11551},[1572,18327,18328],{"class":1638}," openai.embeddings.",[1572,18330,14629],{"class":6311},[1572,18332,12326],{"class":1638},[1572,18334,18335,18338,18341],{"class":1574,"line":10342},[1572,18336,18337],{"class":1638},"    model: ",[1572,18339,18340],{"class":1651},"'text-embedding-3-small'",[1572,18342,1655],{"class":1638},[1572,18344,18345,18348,18351,18353,18355,18357,18359,18362,18364,18367],{"class":1574,"line":10348},[1572,18346,18347],{"class":1638},"    input: text.",[1572,18349,18350],{"class":6311},"replace",[1572,18352,11715],{"class":1638},[1572,18354,8395],{"class":1651},[1572,18356,11969],{"class":1644},[1572,18358,8395],{"class":1651},[1572,18360,18361],{"class":11551},"g",[1572,18363,673],{"class":1638},[1572,18365,18366],{"class":1651},"' '",[1572,18368,17816],{"class":1638},[1572,18370,18371],{"class":1574,"line":11656},[1572,18372,12630],{"class":1638},[1572,18374,18375,18377,18380,18382],{"class":1574,"line":11661},[1572,18376,12639],{"class":11551},[1572,18378,18379],{"class":1638}," response.data[",[1572,18381,1879],{"class":1644},[1572,18383,18384],{"class":1638},"].embedding;\n",[1572,18386,18387],{"class":1574,"line":11671},[1572,18388,1670],{"class":1638},[1572,18390,18391],{"class":1574,"line":11683},[1572,18392,10309],{"emptyLinePlaceholder":396},[1572,18394,18395,18397,18399,18401,18404,18406,18409,18411,18413,18415,18417,18419,18421,18423],{"class":1574,"line":11695},[1572,18396,3790],{"class":11551},[1572,18398,14583],{"class":11551},[1572,18400,7934],{"class":11551},[1572,18402,18403],{"class":6311}," findRelevantContext",[1572,18405,11715],{"class":1638},[1572,18407,18408],{"class":11583},"queryText",[1572,18410,8342],{"class":11551},[1572,18412,11601],{"class":1644},[1572,18414,219],{"class":1638},[1572,18416,8342],{"class":11551},[1572,18418,14603],{"class":6311},[1572,18420,6305],{"class":1638},[1572,18422,14608],{"class":1644},[1572,18424,14611],{"class":1638},[1572,18426,18427,18429,18432,18434,18436,18438],{"class":1574,"line":11700},[1572,18428,11745],{"class":11551},[1572,18430,18431],{"class":1644}," queryVector",[1572,18433,11751],{"class":11551},[1572,18435,14623],{"class":11551},[1572,18437,18293],{"class":6311},[1572,18439,18440],{"class":1638},"(queryText);\n",[1572,18442,18443,18445,18448,18450,18453],{"class":1574,"line":11705},[1572,18444,11745],{"class":11551},[1572,18446,18447],{"class":1644}," sql",[1572,18449,11751],{"class":11551},[1572,18451,18452],{"class":1651}," `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`",[1572,18454,11563],{"class":1638},[1572,18456,18457],{"class":1574,"line":11742},[1572,18458,11790],{"class":1638},[1572,18460,18461,18463,18465,18467,18469,18472,18474],{"class":1574,"line":11763},[1572,18462,11745],{"class":11551},[1572,18464,16981],{"class":1644},[1572,18466,11751],{"class":11551},[1572,18468,14623],{"class":11551},[1572,18470,18471],{"class":1638}," pool.",[1572,18473,17006],{"class":6311},[1572,18475,11887],{"class":1638},[1572,18477,18478,18480],{"class":1574,"line":11787},[1572,18479,12185],{"class":11551},[1572,18481,11578],{"class":1638},[1572,18483,18484,18486,18488,18490,18492,18494,18496,18499,18501,18503,18506],{"class":1574,"line":11793},[1572,18485,12193],{"class":11551},[1572,18487,17080],{"class":1644},[1572,18489,11751],{"class":11551},[1572,18491,14623],{"class":11551},[1572,18493,17003],{"class":1638},[1572,18495,16182],{"class":6311},[1572,18497,18498],{"class":1638},"(sql, [",[1572,18500,3906],{"class":1644},[1572,18502,507],{"class":1638},[1572,18504,18505],{"class":6311},"stringify",[1572,18507,18508],{"class":1638},"(queryVector)]);\n",[1572,18510,18511,18513,18516,18519,18522,18525],{"class":1574,"line":11806},[1572,18512,12487],{"class":11551},[1572,18514,18515],{"class":1638}," (result.rows.",[1572,18517,18518],{"class":1644},"length",[1572,18520,18521],{"class":11551}," ===",[1572,18523,18524],{"class":1644}," 0",[1572,18526,15641],{"class":1638},[1572,18528,18529,18531,18534],{"class":1574,"line":11820},[1572,18530,15540],{"class":11551},[1572,18532,18533],{"class":1651}," ''",[1572,18535,11563],{"class":1638},[1572,18537,18538],{"class":1574,"line":11826},[1572,18539,12625],{"class":1638},[1572,18541,18542],{"class":1574,"line":11831},[1572,18543,18544],{"class":11834},"    \u002F\u002F Combine top matches into a single text block\n",[1572,18546,18547,18549,18552,18554,18556,18559,18561,18564,18566,18568,18570,18572,18574,18576,18578],{"class":1574,"line":11838},[1572,18548,11809],{"class":11551},[1572,18550,18551],{"class":1638}," result.rows.",[1572,18553,11920],{"class":6311},[1572,18555,11715],{"class":1638},[1572,18557,18558],{"class":11583},"row",[1572,18560,11870],{"class":11551},[1572,18562,18563],{"class":1638}," row.content).",[1572,18565,11961],{"class":6311},[1572,18567,11715],{"class":1638},[1572,18569,11966],{"class":1651},[1572,18571,11969],{"class":1644},[1572,18573,1181],{"class":1651},[1572,18575,11969],{"class":1644},[1572,18577,11966],{"class":1651},[1572,18579,11784],{"class":1638},[1572,18581,18582,18584,18586],{"class":1574,"line":11890},[1572,18583,12264],{"class":1638},[1572,18585,17158],{"class":11551},[1572,18587,11578],{"class":1638},[1572,18589,18590,18593,18596],{"class":1574,"line":11895},[1572,18591,18592],{"class":1638},"    client.",[1572,18594,18595],{"class":6311},"release",[1572,18597,11887],{"class":1638},[1572,18599,18600],{"class":1574,"line":11901},[1572,18601,11823],{"class":1638},[1572,18603,18604],{"class":1574,"line":11914},[1572,18605,1670],{"class":1638},[15,18607,18609],{"id":18608},"_3-strict-prompting-fallback-logic","3. Strict Prompting & Fallback Logic",[11,18611,18612],{},"To avoid AI hallucinations, we enforce strict rules inside the system prompt:",[1564,18614,18616],{"className":11542,"code":18615,"language":11544,"meta":363,"style":363},"export async function generateAIResponse(userQuestion: string, context: string): Promise\u003Cstring> {\n  if (!context) {\n    return 'ERROR_UNRESOLVED';\n  }\n\n  const systemPrompt = `\nYou are a technical support assistant for TeleGo.io.\nAnswer the customer's question using ONLY the context provided below.\nRules:\n1. Rely ONLY on the context details. Do not use external knowledge or invent facts.\n2. If the context is insufficient or unrelated to the question, respond strictly with: \"ERROR_UNRESOLVED\".\n3. Maintain a polite, professional tone.\n\nContext:\n---\n${context}\n---\n`;\n\n  const response = await openai.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [\n      { role: 'system', content: systemPrompt },\n      { role: 'user', content: userQuestion }\n    ],\n    temperature: 0.0, \u002F\u002F Force deterministic outputs\n  });\n\n  return response.choices[0].message.content || 'ERROR_UNRESOLVED';\n}\n",[49,18617,18618,18659,18670,18679,18683,18687,18698,18703,18708,18713,18718,18723,18728,18732,18737,18742,18751,18755,18762,18766,18783,18792,18797,18808,18817,18821,18834,18838,18842,18860],{"__ignoreMap":363},[1572,18619,18620,18622,18624,18626,18629,18631,18634,18636,18638,18640,18643,18645,18647,18649,18651,18653,18655,18657],{"class":1574,"line":1575},[1572,18621,3790],{"class":11551},[1572,18623,14583],{"class":11551},[1572,18625,7934],{"class":11551},[1572,18627,18628],{"class":6311}," generateAIResponse",[1572,18630,11715],{"class":1638},[1572,18632,18633],{"class":11583},"userQuestion",[1572,18635,8342],{"class":11551},[1572,18637,11601],{"class":1644},[1572,18639,673],{"class":1638},[1572,18641,18642],{"class":11583},"context",[1572,18644,8342],{"class":11551},[1572,18646,11601],{"class":1644},[1572,18648,219],{"class":1638},[1572,18650,8342],{"class":11551},[1572,18652,14603],{"class":6311},[1572,18654,6305],{"class":1638},[1572,18656,14608],{"class":1644},[1572,18658,14611],{"class":1638},[1572,18660,18661,18663,18665,18667],{"class":1574,"line":364},[1572,18662,11796],{"class":11551},[1572,18664,4110],{"class":1638},[1572,18666,5583],{"class":11551},[1572,18668,18669],{"class":1638},"context) {\n",[1572,18671,18672,18674,18677],{"class":1574,"line":370},[1572,18673,11809],{"class":11551},[1572,18675,18676],{"class":1651}," 'ERROR_UNRESOLVED'",[1572,18678,11563],{"class":1638},[1572,18680,18681],{"class":1574,"line":1591},[1572,18682,11823],{"class":1638},[1572,18684,18685],{"class":1574,"line":1270},[1572,18686,10309],{"emptyLinePlaceholder":396},[1572,18688,18689,18691,18694,18696],{"class":1574,"line":4389},[1572,18690,11745],{"class":11551},[1572,18692,18693],{"class":1644}," systemPrompt",[1572,18695,11751],{"class":11551},[1572,18697,17638],{"class":1651},[1572,18699,18700],{"class":1574,"line":742},[1572,18701,18702],{"class":1651},"You are a technical support assistant for TeleGo.io.\n",[1572,18704,18705],{"class":1574,"line":10336},[1572,18706,18707],{"class":1651},"Answer the customer's question using ONLY the context provided below.\n",[1572,18709,18710],{"class":1574,"line":10342},[1572,18711,18712],{"class":1651},"Rules:\n",[1572,18714,18715],{"class":1574,"line":10348},[1572,18716,18717],{"class":1651},"1. Rely ONLY on the context details. Do not use external knowledge or invent facts.\n",[1572,18719,18720],{"class":1574,"line":11656},[1572,18721,18722],{"class":1651},"2. If the context is insufficient or unrelated to the question, respond strictly with: \"ERROR_UNRESOLVED\".\n",[1572,18724,18725],{"class":1574,"line":11661},[1572,18726,18727],{"class":1651},"3. Maintain a polite, professional tone.\n",[1572,18729,18730],{"class":1574,"line":11671},[1572,18731,10309],{"emptyLinePlaceholder":396},[1572,18733,18734],{"class":1574,"line":11683},[1572,18735,18736],{"class":1651},"Context:\n",[1572,18738,18739],{"class":1574,"line":11695},[1572,18740,18741],{"class":1651},"---\n",[1572,18743,18744,18747,18749],{"class":1574,"line":11700},[1572,18745,18746],{"class":1651},"${",[1572,18748,18642],{"class":1638},[1572,18750,1670],{"class":1651},[1572,18752,18753],{"class":1574,"line":11705},[1572,18754,18741],{"class":1651},[1572,18756,18757,18760],{"class":1574,"line":11742},[1572,18758,18759],{"class":1651},"`",[1572,18761,11563],{"class":1638},[1572,18763,18764],{"class":1574,"line":11763},[1572,18765,10309],{"emptyLinePlaceholder":396},[1572,18767,18768,18770,18772,18774,18776,18779,18781],{"class":1574,"line":11787},[1572,18769,11745],{"class":11551},[1572,18771,18321],{"class":1644},[1572,18773,11751],{"class":11551},[1572,18775,14623],{"class":11551},[1572,18777,18778],{"class":1638}," openai.chat.completions.",[1572,18780,14629],{"class":6311},[1572,18782,12326],{"class":1638},[1572,18784,18785,18787,18790],{"class":1574,"line":11793},[1572,18786,18337],{"class":1638},[1572,18788,18789],{"class":1651},"'gpt-4o-mini'",[1572,18791,1655],{"class":1638},[1572,18793,18794],{"class":1574,"line":11806},[1572,18795,18796],{"class":1638},"    messages: [\n",[1572,18798,18799,18802,18805],{"class":1574,"line":11820},[1572,18800,18801],{"class":1638},"      { role: ",[1572,18803,18804],{"class":1651},"'system'",[1572,18806,18807],{"class":1638},", content: systemPrompt },\n",[1572,18809,18810,18812,18814],{"class":1574,"line":11826},[1572,18811,18801],{"class":1638},[1572,18813,12207],{"class":1651},[1572,18815,18816],{"class":1638},", content: userQuestion }\n",[1572,18818,18819],{"class":1574,"line":11831},[1572,18820,14675],{"class":1638},[1572,18822,18823,18826,18829,18831],{"class":1574,"line":11838},[1572,18824,18825],{"class":1638},"    temperature: ",[1572,18827,18828],{"class":1644},"0.0",[1572,18830,673],{"class":1638},[1572,18832,18833],{"class":11834},"\u002F\u002F Force deterministic outputs\n",[1572,18835,18836],{"class":1574,"line":11890},[1572,18837,12630],{"class":1638},[1572,18839,18840],{"class":1574,"line":11895},[1572,18841,10309],{"emptyLinePlaceholder":396},[1572,18843,18844,18846,18849,18851,18854,18856,18858],{"class":1574,"line":11901},[1572,18845,12639],{"class":11551},[1572,18847,18848],{"class":1638}," response.choices[",[1572,18850,1879],{"class":1644},[1572,18852,18853],{"class":1638},"].message.content ",[1572,18855,4834],{"class":11551},[1572,18857,18676],{"class":1651},[1572,18859,11563],{"class":1638},[1572,18861,18862],{"class":1574,"line":11914},[1572,18863,1670],{"class":1638},[15,18865,18867],{"id":18866},"_4-chat-routing-and-human-handover","4. Chat Routing and Human Handover",[11,18869,18870,18871,18874],{},"When a message is received in the Telegram bot, we check if the user is currently flagged as speaking to a human manager. If not, we run the RAG query. If the RAG engine returns ",[49,18872,18873],{},"ERROR_UNRESOLVED",", we transfer the session.",[1564,18876,18878],{"className":11542,"code":18877,"language":11544,"meta":363,"style":363},"import TelegramBot from 'node-telegram-bot-api';\n\nconst bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!, { polling: true });\n\nbot.on('message', async (msg) => {\n  const chatId = msg.chat.id;\n  const userText = msg.text;\n\n  if (!userText) return;\n\n  \u002F\u002F 1. Get current session status from DB\n  const session = await db.getChatSession(chatId);\n\n  \u002F\u002F If in human handling, forward messages to manager Slack\u002FTelegram\n  if (session?.status === 'human_handling') {\n    await forwardToManagers(chatId, msg);\n    return;\n  }\n\n  \u002F\u002F 2. Perform RAG query\n  try {\n    const context = await findRelevantContext(userText);\n    const aiResponse = await generateAIResponse(userText, context);\n\n    \u002F\u002F 3. Escalation check\n    if (aiResponse.trim() === 'ERROR_UNRESOLVED') {\n      \u002F\u002F Set session to human handling in DB\n      await db.updateChatSession(chatId, { status: 'human_handling' });\n      \n      \u002F\u002F Notify support staff via webhook\n      await notifySupportStaff(chatId, userText);\n      \n      await bot.sendMessage(\n        chatId, \n        \"I couldn't find the answer in our documentation. I've transferred your chat to our support team. A representative will respond shortly.\"\n      );\n      return;\n    }\n\n    \u002F\u002F 4. Send AI answer to user\n    await bot.sendMessage(chatId, aiResponse);\n\n  } catch (error) {\n    console.error('RAG workflow error:', error);\n    await bot.sendMessage(chatId, \"Something went wrong. Let me transfer you to a human manager.\");\n    await db.updateChatSession(chatId, { status: 'human_handling' });\n  }\n});\n",[49,18879,18880,18894,18898,18926,18930,18957,18969,18981,18985,19000,19004,19009,19027,19031,19036,19050,19060,19066,19070,19074,19079,19085,19101,19117,19121,19126,19145,19150,19168,19172,19177,19187,19191,19203,19208,19213,19218,19224,19228,19232,19237,19248,19252,19260,19275,19291,19305,19309],{"__ignoreMap":363},[1572,18881,18882,18884,18887,18889,18892],{"class":1574,"line":1575},[1572,18883,5892],{"class":11551},[1572,18885,18886],{"class":1638}," TelegramBot ",[1572,18888,11557],{"class":11551},[1572,18890,18891],{"class":1651}," 'node-telegram-bot-api'",[1572,18893,11563],{"class":1638},[1572,18895,18896],{"class":1574,"line":364},[1572,18897,10309],{"emptyLinePlaceholder":396},[1572,18899,18900,18902,18905,18907,18909,18912,18914,18917,18919,18922,18924],{"class":1574,"line":370},[1572,18901,5852],{"class":11551},[1572,18903,18904],{"class":1644}," bot",[1572,18906,11751],{"class":11551},[1572,18908,11754],{"class":11551},[1572,18910,18911],{"class":6311}," TelegramBot",[1572,18913,14497],{"class":1638},[1572,18915,18916],{"class":1644},"TELEGRAM_BOT_TOKEN",[1572,18918,5583],{"class":11551},[1572,18920,18921],{"class":1638},", { polling: ",[1572,18923,3638],{"class":1644},[1572,18925,15938],{"class":1638},[1572,18927,18928],{"class":1574,"line":1591},[1572,18929,10309],{"emptyLinePlaceholder":396},[1572,18931,18932,18935,18937,18939,18942,18944,18946,18948,18951,18953,18955],{"class":1574,"line":1270},[1572,18933,18934],{"class":1638},"bot.",[1572,18936,3650],{"class":6311},[1572,18938,11715],{"class":1638},[1572,18940,18941],{"class":1651},"'message'",[1572,18943,673],{"class":1638},[1572,18945,16020],{"class":11551},[1572,18947,4110],{"class":1638},[1572,18949,18950],{"class":11583},"msg",[1572,18952,15497],{"class":1638},[1572,18954,12459],{"class":11551},[1572,18956,11578],{"class":1638},[1572,18958,18959,18961,18964,18966],{"class":1574,"line":4389},[1572,18960,11745],{"class":11551},[1572,18962,18963],{"class":1644}," chatId",[1572,18965,11751],{"class":11551},[1572,18967,18968],{"class":1638}," msg.chat.id;\n",[1572,18970,18971,18973,18976,18978],{"class":1574,"line":742},[1572,18972,11745],{"class":11551},[1572,18974,18975],{"class":1644}," userText",[1572,18977,11751],{"class":11551},[1572,18979,18980],{"class":1638}," msg.text;\n",[1572,18982,18983],{"class":1574,"line":10336},[1572,18984,10309],{"emptyLinePlaceholder":396},[1572,18986,18987,18989,18991,18993,18996,18998],{"class":1574,"line":10342},[1572,18988,11796],{"class":11551},[1572,18990,4110],{"class":1638},[1572,18992,5583],{"class":11551},[1572,18994,18995],{"class":1638},"userText) ",[1572,18997,16305],{"class":11551},[1572,18999,11563],{"class":1638},[1572,19001,19002],{"class":1574,"line":10348},[1572,19003,10309],{"emptyLinePlaceholder":396},[1572,19005,19006],{"class":1574,"line":11656},[1572,19007,19008],{"class":11834},"  \u002F\u002F 1. Get current session status from DB\n",[1572,19010,19011,19013,19015,19017,19019,19021,19024],{"class":1574,"line":11661},[1572,19012,11745],{"class":11551},[1572,19014,14618],{"class":1644},[1572,19016,11751],{"class":11551},[1572,19018,14623],{"class":11551},[1572,19020,16179],{"class":1638},[1572,19022,19023],{"class":6311},"getChatSession",[1572,19025,19026],{"class":1638},"(chatId);\n",[1572,19028,19029],{"class":1574,"line":11671},[1572,19030,10309],{"emptyLinePlaceholder":396},[1572,19032,19033],{"class":1574,"line":11683},[1572,19034,19035],{"class":11834},"  \u002F\u002F If in human handling, forward messages to manager Slack\u002FTelegram\n",[1572,19037,19038,19040,19043,19045,19048],{"class":1574,"line":11695},[1572,19039,11796],{"class":11551},[1572,19041,19042],{"class":1638}," (session?.status ",[1572,19044,12132],{"class":11551},[1572,19046,19047],{"class":1651}," 'human_handling'",[1572,19049,15641],{"class":1638},[1572,19051,19052,19054,19057],{"class":1574,"line":11700},[1572,19053,17028],{"class":11551},[1572,19055,19056],{"class":6311}," forwardToManagers",[1572,19058,19059],{"class":1638},"(chatId, msg);\n",[1572,19061,19062,19064],{"class":1574,"line":11705},[1572,19063,11809],{"class":11551},[1572,19065,11563],{"class":1638},[1572,19067,19068],{"class":1574,"line":11742},[1572,19069,11823],{"class":1638},[1572,19071,19072],{"class":1574,"line":11763},[1572,19073,10309],{"emptyLinePlaceholder":396},[1572,19075,19076],{"class":1574,"line":11787},[1572,19077,19078],{"class":11834},"  \u002F\u002F 2. Perform RAG query\n",[1572,19080,19081,19083],{"class":1574,"line":11793},[1572,19082,12185],{"class":11551},[1572,19084,11578],{"class":1638},[1572,19086,19087,19089,19092,19094,19096,19098],{"class":1574,"line":11806},[1572,19088,12193],{"class":11551},[1572,19090,19091],{"class":1644}," context",[1572,19093,11751],{"class":11551},[1572,19095,14623],{"class":11551},[1572,19097,18403],{"class":6311},[1572,19099,19100],{"class":1638},"(userText);\n",[1572,19102,19103,19105,19108,19110,19112,19114],{"class":1574,"line":11820},[1572,19104,12193],{"class":11551},[1572,19106,19107],{"class":1644}," aiResponse",[1572,19109,11751],{"class":11551},[1572,19111,14623],{"class":11551},[1572,19113,18628],{"class":6311},[1572,19115,19116],{"class":1638},"(userText, context);\n",[1572,19118,19119],{"class":1574,"line":11826},[1572,19120,10309],{"emptyLinePlaceholder":396},[1572,19122,19123],{"class":1574,"line":11831},[1572,19124,19125],{"class":11834},"    \u002F\u002F 3. Escalation check\n",[1572,19127,19128,19130,19133,19136,19139,19141,19143],{"class":1574,"line":11838},[1572,19129,12487],{"class":11551},[1572,19131,19132],{"class":1638}," (aiResponse.",[1572,19134,19135],{"class":6311},"trim",[1572,19137,19138],{"class":1638},"() ",[1572,19140,12132],{"class":11551},[1572,19142,18676],{"class":1651},[1572,19144,15641],{"class":1638},[1572,19146,19147],{"class":1574,"line":11890},[1572,19148,19149],{"class":11834},"      \u002F\u002F Set session to human handling in DB\n",[1572,19151,19152,19155,19157,19160,19163,19166],{"class":1574,"line":11895},[1572,19153,19154],{"class":11551},"      await",[1572,19156,16179],{"class":1638},[1572,19158,19159],{"class":6311},"updateChatSession",[1572,19161,19162],{"class":1638},"(chatId, { status: ",[1572,19164,19165],{"class":1651},"'human_handling'",[1572,19167,15938],{"class":1638},[1572,19169,19170],{"class":1574,"line":11901},[1572,19171,12518],{"class":1638},[1572,19173,19174],{"class":1574,"line":11914},[1572,19175,19176],{"class":11834},"      \u002F\u002F Notify support staff via webhook\n",[1572,19178,19179,19181,19184],{"class":1574,"line":11956},[1572,19180,19154],{"class":11551},[1572,19182,19183],{"class":6311}," notifySupportStaff",[1572,19185,19186],{"class":1638},"(chatId, userText);\n",[1572,19188,19189],{"class":1574,"line":11976},[1572,19190,12518],{"class":1638},[1572,19192,19193,19195,19198,19201],{"class":1574,"line":11981},[1572,19194,19154],{"class":11551},[1572,19196,19197],{"class":1638}," bot.",[1572,19199,19200],{"class":6311},"sendMessage",[1572,19202,15437],{"class":1638},[1572,19204,19205],{"class":1574,"line":11987},[1572,19206,19207],{"class":1638},"        chatId, \n",[1572,19209,19210],{"class":1574,"line":11993},[1572,19211,19212],{"class":1651},"        \"I couldn't find the answer in our documentation. I've transferred your chat to our support team. A representative will respond shortly.\"\n",[1572,19214,19215],{"class":1574,"line":12006},[1572,19216,19217],{"class":1638},"      );\n",[1572,19219,19220,19222],{"class":1574,"line":12026},[1572,19221,15540],{"class":11551},[1572,19223,11563],{"class":1638},[1572,19225,19226],{"class":1574,"line":12037},[1572,19227,12625],{"class":1638},[1572,19229,19230],{"class":1574,"line":12047},[1572,19231,10309],{"emptyLinePlaceholder":396},[1572,19233,19234],{"class":1574,"line":12052},[1572,19235,19236],{"class":11834},"    \u002F\u002F 4. Send AI answer to user\n",[1572,19238,19239,19241,19243,19245],{"class":1574,"line":12058},[1572,19240,17028],{"class":11551},[1572,19242,19197],{"class":1638},[1572,19244,19200],{"class":6311},[1572,19246,19247],{"class":1638},"(chatId, aiResponse);\n",[1572,19249,19250],{"class":1574,"line":12070},[1572,19251,10309],{"emptyLinePlaceholder":396},[1572,19253,19254,19256,19258],{"class":1574,"line":12084},[1572,19255,12264],{"class":1638},[1572,19257,12267],{"class":11551},[1572,19259,12270],{"class":1638},[1572,19261,19262,19265,19267,19269,19272],{"class":1574,"line":12094},[1572,19263,19264],{"class":1638},"    console.",[1572,19266,15649],{"class":6311},[1572,19268,11715],{"class":1638},[1572,19270,19271],{"class":1651},"'RAG workflow error:'",[1572,19273,19274],{"class":1638},", error);\n",[1572,19276,19277,19279,19281,19283,19286,19289],{"class":1574,"line":12108},[1572,19278,17028],{"class":11551},[1572,19280,19197],{"class":1638},[1572,19282,19200],{"class":6311},[1572,19284,19285],{"class":1638},"(chatId, ",[1572,19287,19288],{"class":1651},"\"Something went wrong. Let me transfer you to a human manager.\"",[1572,19290,11784],{"class":1638},[1572,19292,19293,19295,19297,19299,19301,19303],{"class":1574,"line":12113},[1572,19294,17028],{"class":11551},[1572,19296,16179],{"class":1638},[1572,19298,19159],{"class":6311},[1572,19300,19162],{"class":1638},[1572,19302,19165],{"class":1651},[1572,19304,15938],{"class":1638},[1572,19306,19307],{"class":1574,"line":12119},[1572,19308,11823],{"class":1638},[1572,19310,19311],{"class":1574,"line":12138},[1572,19312,13911],{"class":1638},[11,19314,19315,19316,19318],{},"In my project ",[38,19317,12706],{"href":12705},", we built a similar RAG system to handle initial customer questions regarding billing or scenario configurations. When a user asks a complex technical question that isn't answered in our docs, the system automatically redirects the ticket to the manager dashboard.",[11,19320,19321,19322,19326,19327,507],{},"This escalation flow is even more powerful when paired with ",[38,19323,19325],{"href":19324},"\u002Fblog\u002Ftelegram-bot-crm-integration","Telegram Bot CRM Integrations"," to automatically log these events. Furthermore, you can host the manager communication dashboard directly inside a ",[38,19328,19329],{"href":16499},"Telegram Web App",[15,19331,12723],{"id":12722},[298,19333,19334,19342,19349],{},[301,19335,19336,19341],{},[38,19337,19340],{"href":19338,"rel":19339},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fembeddings",[429],"OpenAI Embeddings"," — vector generation, including the text-embedding-3-small model from the example",[301,19343,19344,19348],{},[38,19345,18008],{"href":19346,"rel":19347},"https:\u002F\u002Fgithub.com\u002Fpgvector\u002Fpgvector",[429]," — the PostgreSQL extension for vector search",[301,19350,19351,19356],{},[38,19352,19355],{"href":19353,"rel":19354},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fapi",[429],"Telegram Bot API"," — receiving messages and long polling",[11,19358,19359],{},"Implementing RAG alongside an automated escalation policy lets the AI close routine tickets while humans join only the complex dialogues — cutting support costs many times over without sacrificing service quality.",[11,19361,19362,19363,13330,19365,13334],{},"If you want to implement a custom AI support chatbot, set up PostgreSQL Pgvector indexing, or build a complex CRM-integrated helpdesk, learn more about my ",[38,19364,12751],{"href":12750},[38,19366,13333],{"href":12755},[1779,19368,19369],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":363,"searchDepth":364,"depth":364,"links":19371},[19372,19373,19374,19375,19376,19377],{"id":18012,"depth":364,"text":18013},{"id":18022,"depth":364,"text":18023},{"id":18193,"depth":364,"text":18194},{"id":18608,"depth":364,"text":18609},{"id":18866,"depth":364,"text":18867},{"id":12722,"depth":364,"text":12723},"2026-06-04","A technical guide on engineering a professional customer support Telegram bot using OpenAI Embeddings, PostgreSQL Pgvector, and a hybrid AI-to-human escalation workflow.",[19381,19384,19387],{"q":19382,"a":19383},"How is a RAG bot different from 'just ChatGPT in a bot'?","A RAG bot answers strictly from your knowledge base: it first retrieves relevant documentation fragments via vector search, then passes them to the model as context. This removes the two main problems of a bare LLM — hallucinations and ignorance of your prices, guides, and API.",{"q":19385,"a":19386},"What happens when the bot doesn't know the answer?","The escalation protocol kicks in: if vector search finds no relevant context or the model returns an uncertainty marker, the session is flagged human_handling, managers get notified, and every following client message is forwarded to a live operator.",{"q":19388,"a":19389},"What does running such a bot cost?","Two cost lines: embedding generation when indexing the knowledge base (one-off and cheap) and LLM calls per question, billed by actual API usage. On a typical question flow this is well below a support operator's rate.","\u002Fimages\u002Fblog\u002Fblog_ai_rag_support.webp",{},"\u002Fblog\u002Fen\u002Ftelegram-bot-ai-rag-support",{"title":17979,"description":19379},"blog\u002Fen\u002Ftelegram-bot-ai-rag-support",[12790,13369,19396,19397,12793,17291],"RAG","LLM","_j44FUxWZFswLek5sf3BV0mXUZD_-voO1X_v9PiAvkU",{"id":19400,"title":19401,"body":19402,"date":19673,"description":19674,"extension":376,"faq":19675,"image":19688,"lang":394,"meta":19689,"navigation":396,"path":19691,"published":396,"readTime":364,"seo":19692,"stem":19693,"tags":19694,"updated":12772,"__hash__":19698},"blog\u002Fblog\u002Fen\u002Fbot-builder-vs-custom-development.md","Bot Builder vs Custom Development: How to Choose in 2026",{"type":8,"value":19403,"toc":19667},[19404,19407,19417,19421,19542,19546,19549,19575,19578,19582,19585,19623,19631,19635,19638,19649,19656],[11,19405,19406],{},"The short answer: if your scenario fits standard blocks — booking, payments, menus, broadcasts — take a no-code builder and launch in a day. If you need non-standard logic, deep integrations, or high load — order custom development from $250. Below is a decision table and where exactly the boundary runs.",[11,19408,19409,19410,19412,19413,19416],{},"I'm in an unusual position for this comparison: I'm both the author of the no-code builder ",[38,19411,12706],{"href":12705}," and a developer of ",[38,19414,19415],{"href":12750},"custom Telegram bots",". I have no incentive to oversell either option — here's how I choose myself.",[15,19418,19420],{"id":19419},"builder-vs-custom-the-decision-table","Builder vs custom: the decision table",[75,19422,19423,19435],{},[78,19424,19425],{},[81,19426,19427,19429,19432],{},[84,19428,5172],{"align":12854},[84,19430,19431],{"align":12854},"No-code builder",[84,19433,19434],{"align":12854},"Custom development",[91,19436,19437,19450,19463,19477,19490,19503,19516,19529],{},[81,19438,19439,19444,19447],{},[96,19440,19441],{"align":12854},[26,19442,19443],{},"Launch time",[96,19445,19446],{"align":12854},"Hours — 1 day",[96,19448,19449],{"align":12854},"3–14 days",[81,19451,19452,19457,19460],{},[96,19453,19454],{"align":12854},[26,19455,19456],{},"Starting price",[96,19458,19459],{"align":12854},"Subscription from a few $\u002Fmonth",[96,19461,19462],{"align":12854},"From $250 one-time",[81,19464,19465,19471,19474],{},[96,19466,19467,19470],{"align":12854},[26,19468,19469],{},"Standard flows"," (booking, payments, FAQ)",[96,19472,19473],{"align":12854},"✅ Ready-made blocks",[96,19475,19476],{"align":12854},"✅ But more than you need to pay",[81,19478,19479,19484,19487],{},[96,19480,19481],{"align":12854},[26,19482,19483],{},"Non-standard logic",[96,19485,19486],{"align":12854},"❌ You'll hit block limits",[96,19488,19489],{"align":12854},"✅ Anything",[81,19491,19492,19497,19500],{},[96,19493,19494],{"align":12854},[26,19495,19496],{},"Integrations",[96,19498,19499],{"align":12854},"Popular ones out of the box (CRM, sheets, payments)",[96,19501,19502],{"align":12854},"Any, including in-house systems",[81,19504,19505,19510,19513],{},[96,19506,19507],{"align":12854},[26,19508,19509],{},"High load",[96,19511,19512],{"align":12854},"Depends on the platform",[96,19514,19515],{"align":12854},"✅ Architecture built for the task",[81,19517,19518,19523,19526],{},[96,19519,19520],{"align":12854},[26,19521,19522],{},"Edits without a developer",[96,19524,19525],{"align":12854},"✅ Everything is yours to edit",[96,19527,19528],{"align":12854},"✅ Content yourself, logic via a developer",[81,19530,19531,19536,19539],{},[96,19532,19533],{"align":12854},[26,19534,19535],{},"Ownership",[96,19537,19538],{"align":12854},"The bot lives on the platform",[96,19540,19541],{"align":12854},"Code and data are fully yours",[15,19543,19545],{"id":19544},"when-a-builder-is-enough","When a builder is enough",[11,19547,19548],{},"From my experience running TeleGo, standard blocks cover most small-business tasks:",[298,19550,19551,19557,19563,19569],{},[301,19552,19553,19556],{},[26,19554,19555],{},"Client booking"," — salons, studios, private practice;",[301,19558,19559,19562],{},[26,19560,19561],{},"Selling access and courses"," — Stripe and Telegram Stars payments inside the chat;",[301,19564,19565,19568],{},[26,19566,19567],{},"Lead collection and nurturing"," — surveys, segmentation, automated reminders;",[301,19570,19571,19574],{},[26,19572,19573],{},"Support"," — FAQ answers, including AI replies from your own knowledge base.",[11,19576,19577],{},"If your task is on this list, don't overpay for development: assemble the bot in a visual editor, validate demand, and only then decide whether you need custom. TeleGo has a free 7-day Pro trial exactly for that.",[15,19579,19581],{"id":19580},"when-you-need-custom","When you need custom",[11,19583,19584],{},"Custom development is justified when at least one of these appears:",[4001,19586,19587,19596,19602,19611,19617],{},[301,19588,19589,19592,19593,507],{},[26,19590,19591],{},"Non-standard business logic"," — lead scoring, complex branching, calculations. A code-level example is in my article on ",[38,19594,19595],{"href":19324},"integrating a bot with a CRM",[301,19597,19598,19601],{},[26,19599,19600],{},"Deep integrations"," — an in-house CRM, ERP, niche APIs that no builder supports.",[301,19603,19604,19607,19608,8399],{},[26,19605,19606],{},"AI on your data"," — a RAG bot that answers strictly from your knowledge base and hands complex dialogues to a human (",[38,19609,19610],{"href":12710},"architecture breakdown",[301,19612,19613,19616],{},[26,19614,19615],{},"Load"," — tens of thousands of users require purpose-built queues and database architecture.",[301,19618,19619,19622],{},[26,19620,19621],{},"Code ownership requirements"," — corporate policies or investors.",[11,19624,19625,19626,19630],{},"What it costs by bot type is in my ",[38,19627,19629],{"href":19628},"\u002Fblog\u002Ftelegram-bot-development-cost","detailed development cost breakdown","; the short version: a simple bot from $250, a business bot from $500, an AI bot from $900 — all with a control panel and 6 months of hosting included.",[15,19632,19634],{"id":19633},"the-hybrid-path-how-i-do-it","The hybrid path: how I do it",[11,19636,19637],{},"The most common mistake is choosing \"forever.\" The working scheme looks different:",[4001,19639,19640,19643,19646],{},[301,19641,19642],{},"Assemble an MVP bot on a builder in a day and launch it on real clients.",[301,19644,19645],{},"Collect data: which flows get used, where users drop off.",[301,19647,19648],{},"When you hit the ceiling — order custom, porting flows already proven by demand, not hypotheses.",[11,19650,19651,19652,19655],{},"This way you pay for development once, and only for logic validated by real usage. Technically the bot talks to the ",[38,19653,19355],{"href":19353,"rel":19654},[429]," in both cases — migration means porting flows and the client database, not starting over.",[11,19657,19658,19659,19662,19663,19666],{},"Not sure which path is yours? Describe the task on the ",[38,19660,19661],{"href":12750},"Telegram bot development page"," — I'll tell you honestly if a builder is enough, or book a ",[38,19664,19665],{"href":12755},"consultation"," for a scenario review.",{"title":363,"searchDepth":364,"depth":364,"links":19668},[19669,19670,19671,19672],{"id":19419,"depth":364,"text":19420},{"id":19544,"depth":364,"text":19545},{"id":19580,"depth":364,"text":19581},{"id":19633,"depth":364,"text":19634},"2026-05-31","An honest comparison of no-code Telegram bot builders and custom development — price, timelines, flexibility, and the capability ceiling. Decision table inside.",[19676,19679,19682,19685],{"q":19677,"a":19678},"When is a bot builder enough?","When your scenario fits standard blocks: welcome flow, menu, booking, payments, broadcasts, FAQ answers. That covers 80% of small-business tasks — salons, schools, consultants, e-commerce.",{"q":19680,"a":19681},"Can I migrate from a builder to custom development later?","Yes, and it's a normal path: validate demand on a builder within a day, and when you hit its limits, order a custom bot and port the proven flows. Just make sure you can export your client database.",{"q":19683,"a":19684},"Which is cheaper over a year?","For standard scenarios — a builder: the subscription costs less than maintaining custom code. For non-standard logic — custom: workarounds fighting a builder's limits cost more than development.",{"q":19686,"a":19687},"Who maintains the bot after launch?","On a builder — you do, through the visual editor. With custom development — you edit content through a control panel, while a developer maintains the logic and integrations.","\u002Fimages\u002Fblog\u002Fblog_bot_builder_vs_custom.webp",{"robots":19690},"noindex, nofollow","\u002Fblog\u002Fen\u002Fbot-builder-vs-custom-development",{"title":19401,"description":19674},"blog\u002Fen\u002Fbot-builder-vs-custom-development",[12790,19695,19696,19697],"Bots","No-code","Business","a8B8mQrOJfE8-ZydYEdO5SXDI96I2VfPLmTx2d6MXZg",{"id":19700,"title":19701,"body":19702,"date":19949,"description":19950,"extension":376,"faq":19951,"image":19964,"lang":394,"meta":19965,"navigation":396,"path":19966,"published":396,"readTime":370,"seo":19967,"stem":19968,"tags":19969,"updated":12772,"__hash__":19970},"blog\u002Fblog\u002Fen\u002Ftelegram-bot-development-cost.md","How Much Does Telegram Bot Development Cost in 2026: A Real Price Breakdown",{"type":8,"value":19703,"toc":19942},[19704,19707,19711,19719,19743,19747,19750,19817,19820,19824,19827,19852,19856,19859,19895,19898,19902,19926,19933],[11,19705,19706],{},"Turnkey Telegram bot development costs from $250 for a simple menu-and-leads bot to $900+ for a smart AI bot integrated with your CRM. The final price depends on three things: dialogue complexity, the number of integrations, and load requirements. This article is an honest cost breakdown by bot type — what the price includes and which expenses appear after launch.",[15,19708,19710],{"id":19709},"what-drives-the-cost-of-a-telegram-bot","What drives the cost of a Telegram bot?",[11,19712,19713,19714,19718],{},"You are paying for the bot's logic, not for \"code that sends messages.\" The Telegram Bot API itself is free and well documented (",[38,19715,19717],{"href":19353,"rel":19716},[429],"official documentation","), so the budget goes to:",[4001,19720,19721,19727,19732,19738],{},[301,19722,19723,19726],{},[26,19724,19725],{},"Flows"," — how many dialogue branches, states, and edge cases need to be designed and tested.",[301,19728,19729,19731],{},[26,19730,19496],{}," — payments (Stripe, Telegram Stars, crypto), CRMs (HubSpot, amoCRM), Google Sheets and Calendar, AI models.",[301,19733,19734,19737],{},[26,19735,19736],{},"Control panel"," — whether the owner can edit content independently, or every small change requires a developer.",[301,19739,19740,19742],{},[26,19741,19615],{}," — a bot for a salon with 200 clients and a bot for an online school with 20,000 students are built differently.",[15,19744,19746],{"id":19745},"prices-by-bot-type","Prices by bot type",[11,19748,19749],{},"A breakdown from my real practice — starting prices for a typical scope:",[75,19751,19752,19767],{},[78,19753,19754],{},[81,19755,19756,19759,19762,19765],{},[84,19757,19758],{"align":12854},"Bot type",[84,19760,19761],{"align":12854},"What it does",[84,19763,19764],{"align":12854},"Timeline",[84,19766,170],{"align":12854},[91,19768,19769,19785,19801],{},[81,19770,19771,19776,19779,19782],{},[96,19772,19773],{"align":12854},[26,19774,19775],{},"Simple bot",[96,19777,19778],{"align":12854},"Welcome menu, button navigation, contact collection, lead database",[96,19780,19781],{"align":12854},"3–5 days",[96,19783,19784],{"align":12854},"from $250",[81,19786,19787,19792,19795,19798],{},[96,19788,19789],{"align":12854},[26,19790,19791],{},"Business bot",[96,19793,19794],{"align":12854},"Everything above + booking or payments, surveys, automated reminders, client segmentation",[96,19796,19797],{"align":12854},"7–10 days",[96,19799,19800],{"align":12854},"from $500",[81,19802,19803,19808,19811,19814],{},[96,19804,19805],{"align":12854},[26,19806,19807],{},"Smart AI bot",[96,19809,19810],{"align":12854},"Everything above + auto-answers from your knowledge base, CRM and external integrations, human handover",[96,19812,19813],{"align":12854},"10–14 days",[96,19815,19816],{"align":12854},"from $900",[11,19818,19819],{},"Every package includes: bot configuration, a no-code control panel, testing, launch, and the first 6 months of hosting.",[15,19821,19823],{"id":19822},"which-costs-appear-after-launch","Which costs appear after launch?",[11,19825,19826],{},"The honest answer that rarely makes it into price lists:",[298,19828,19829,19835,19841,19846],{},[301,19830,19831,19834],{},[26,19832,19833],{},"Hosting"," — after the included period, a simple bot lives on a small VPS for a few dollars a month.",[301,19836,19837,19840],{},[26,19838,19839],{},"LLM API usage"," — AI bots generate answers via OpenAI, Claude, or Gemini, billed by actual usage. For a typical support bot this is far below an operator's salary, but the line item should be visible upfront.",[301,19842,19843,19845],{},[26,19844,14155],{}," — Stripe and Telegram Stars take a percentage per transaction; that's a cost of accepting payments, not of the bot.",[301,19847,19848,19851],{},[26,19849,19850],{},"New features"," — additional flow branches and integrations. This is where the control panel pays off: text and button changes you make yourself, for free.",[15,19853,19855],{"id":19854},"when-does-a-bot-pay-for-itself","When does a bot pay for itself?",[11,19857,19858],{},"Channel economics is the main reason businesses order bots instead of yet another landing page:",[298,19860,19861,19872,19883],{},[301,19862,19863,19864,4110,19867,8971],{},"Telegram has over ",[26,19865,19866],{},"1 billion monthly active users",[38,19868,19871],{"href":19869,"rel":19870},"https:\u002F\u002Ftechcrunch.com\u002F2025\u002F03\u002F19\u002Ftelegram-founder-pavel-durov-says-app-now-has-1b-users-calls-whatsapp-a-cheap-watered-down-imitation\u002F",[429],"TechCrunch, March 2025",[301,19873,19874,19875,4110,19878,8971],{},"messenger open rates reach ",[26,19876,19877],{},"80% versus ~20% for email",[38,19879,19882],{"href":19880,"rel":19881},"https:\u002F\u002Fwww.levelingup.com\u002Fgrowth-everywhere-interview\u002Fmikael-yang-manychat\u002F",[429],"ManyChat CEO interview",[301,19884,19885,19886,19889,19890,8399],{},"cost per lead through a bot can be ",[26,19887,19888],{},"up to 7x lower"," than through a classic landing page (",[38,19891,19894],{"href":19892,"rel":19893},"https:\u002F\u002Fblog.chatfuel.com\u002Fchatbots-vs-landing-pages\u002F",[429],"Chatfuel data",[11,19896,19897],{},"A practical benchmark: if a bot saves at least one hour of an administrator's manual work per day, or brings a few qualified leads per week, a $250–900 investment returns within the first months.",[15,19899,19901],{"id":19900},"how-to-save-money-without-cutting-quality","How to save money without cutting quality",[298,19903,19904,19910,19920],{},[301,19905,19906,19909],{},[26,19907,19908],{},"Start with a simple bot"," and add AI and integrations after demand is proven — my bot architecture allows growing features without a rewrite.",[301,19911,19912,19915,19916,507],{},[26,19913,19914],{},"Prepare the scenario in advance",": dialogues described in your own words and a list of real customer questions shorten the design phase. What such a brief should contain is covered in my ",[38,19917,19919],{"href":19918},"\u002Fblog\u002Ftelegram-bot-specification-checklist","bot specification checklist",[301,19921,19922,19925],{},[26,19923,19924],{},"Don't order features \"for later\"",": broadcasts, referral programs, and multi-language support are easy to add as a second phase.",[11,19927,19928,19929,507],{},"Choosing between custom development and a no-code builder? I covered that decision in ",[38,19930,19932],{"href":19931},"\u002Fblog\u002Fbot-builder-vs-custom-development","Bot builder vs custom development",[11,19934,19935,19936,19938,19939,19941],{},"Want an exact estimate for your task? Describe it on the ",[38,19937,19661],{"href":12750}," — I reply within 24 hours with a timeline and budget, or book a ",[38,19940,19665],{"href":12755}," if the task isn't shaped into a spec yet.",{"title":363,"searchDepth":364,"depth":364,"links":19943},[19944,19945,19946,19947,19948],{"id":19709,"depth":364,"text":19710},{"id":19745,"depth":364,"text":19746},{"id":19822,"depth":364,"text":19823},{"id":19854,"depth":364,"text":19855},{"id":19900,"depth":364,"text":19901},"2026-05-28","Real Telegram bot development prices — from a $250 menu bot to an AI bot with CRM. What's included, hidden costs after launch, and when a bot pays off.",[19952,19955,19958,19961],{"q":19953,"a":19954},"How long does it take to build a Telegram bot?","A simple bot with a menu and lead collection ships in 3–5 days. A business bot with booking and payments takes 7–10 days. A smart AI bot with knowledge-base answers and CRM integration takes 10–14 days. Timelines count from an agreed specification to launch.",{"q":19956,"a":19957},"Do I pay for bot hosting separately?","My packages include the first 6 months of hosting in the development price. After that, a simple bot runs on a small VPS for a few dollars a month; AI bots add language-model API usage billed by actual consumption.",{"q":19959,"a":19960},"Can I edit bot texts and flows without a developer?","Yes. Every bot ships with a no-code control panel: texts, buttons, flows, and broadcasts are edited visually. A developer is only needed for new integrations or non-standard logic.",{"q":19962,"a":19963},"What makes the price go above the base package?","Three main factors: non-standard integrations (in-house CRMs, niche payment providers), complex branching dialogue logic, and load requirements — a bot serving tens of thousands of users needs a different queue and database architecture.","\u002Fimages\u002Fblog\u002Fblog_telegram_bot_cost.webp",{},"\u002Fblog\u002Fen\u002Ftelegram-bot-development-cost",{"title":19701,"description":19950},"blog\u002Fen\u002Ftelegram-bot-development-cost",[12790,19695,14252,19697],"hjufOMEwo6WFvMM57VzfqGJc9jRdo1NveicxrzXvC6I",{"id":19972,"title":19973,"body":19974,"date":20216,"description":20217,"extension":376,"faq":20218,"image":20231,"lang":394,"meta":20232,"navigation":396,"path":20233,"published":396,"readTime":364,"seo":20234,"stem":20235,"tags":20236,"updated":12772,"__hash__":20239},"blog\u002Fblog\u002Fen\u002Ftelegram-bot-specification-checklist.md","What a Telegram Bot Specification Should Include: A Checklist with Examples",{"type":8,"value":19975,"toc":20207},[19976,19979,19983,19986,20000,20003,20007,20010,20016,20019,20023,20026,20063,20067,20070,20119,20125,20129,20152,20156,20159,20188,20192,20199],[11,19977,19978],{},"A good task description cuts both the estimate and the timeline of Telegram bot development: the less a developer has to guess, the less risk padding lands in the quote. Below is the 6-block checklist I ask clients to fill in before estimating a project. It also filters out the most common mistake — ordering \"a bot in general\" instead of a bot for a specific process.",[15,19980,19982],{"id":19981},"_1-the-goal-which-metric-does-the-bot-change","1. The goal: which metric does the bot change?",[11,19984,19985],{},"One paragraph, without which everything else is pointless:",[298,19987,19988,19994],{},[301,19989,19990,19993],{},[1321,19991,19992],{},"Bad",": \"We need a bot for our salon.\"",[301,19995,19996,19999],{},[1321,19997,19998],{},"Good",": \"Clients book by phone; the receptionist spends 3 hours a day on it. The bot should handle 70% of bookings without a human.\"",[11,20001,20002],{},"The goal drives both the feature priorities and how we'll know the bot paid off.",[15,20004,20006],{"id":20005},"_2-dialogue-scenarios-step-by-step","2. Dialogue scenarios, step by step",[11,20008,20009],{},"Describe 3–5 main scenarios the way a real client would walk through them:",[1564,20011,20014],{"className":20012,"code":20013,"language":4815,"meta":363},[4813],"Scenario \"Book a service\":\n1. Client taps \u002Fstart → greeting + menu [Book] [Prices] [Question]\n2. [Book] → bot shows the service list (buttons)\n3. Service chosen → bot shows available slots for the week\n4. Slot chosen → bot asks for a phone number (\"Share contact\" button)\n5. Confirmation → calendar entry + notification to the administrator\n6. One day before the visit → automatic reminder to the client\n",[49,20015,20013],{"__ignoreMap":363},[11,20017,20018],{},"This format answers most design questions by itself: which buttons, what data we collect, who gets notified.",[15,20020,20022],{"id":20021},"_3-edge-cases-and-errors","3. Edge cases and errors",[11,20024,20025],{},"The block missing from 9 out of 10 briefs — and the one that defines bot quality:",[298,20027,20028,20034,20041,20051,20057],{},[301,20029,20030,20031,6735],{},"What does the client see when ",[26,20032,20033],{},"all slots are taken",[301,20035,20036,20037,20040],{},"What happens if the client ",[26,20038,20039],{},"abandons the dialogue midway"," and returns a day later?",[301,20042,20043,20044,20047,20048,219],{},"Who answers when the client asks a ",[26,20045,20046],{},"question outside the flow","? (the AI-answers-from-knowledge-base option is covered in my ",[38,20049,20050],{"href":12710},"RAG support bot breakdown",[301,20052,20053,20054,6735],{},"How does the client ",[26,20055,20056],{},"cancel or reschedule",[301,20058,20059,20060,6735],{},"What does the bot do on a ",[26,20061,20062],{},"failed payment",[15,20064,20066],{"id":20065},"_4-integrations-and-data","4. Integrations and data",[11,20068,20069],{},"List the systems the bot must exchange data with, and in which direction:",[75,20071,20072,20085],{},[78,20073,20074],{},[81,20075,20076,20079,20082],{},[84,20077,20078],{"align":12854},"System",[84,20080,20081],{"align":12854},"What we send",[84,20083,20084],{"align":12854},"What we receive",[91,20086,20087,20098,20108],{},[81,20088,20089,20092,20095],{},[96,20090,20091],{"align":12854},"CRM (HubSpot, amoCRM…)",[96,20093,20094],{"align":12854},"Contact, deal, survey answers",[96,20096,20097],{"align":12854},"Statuses, client history",[81,20099,20100,20103,20105],{},[96,20101,20102],{"align":12854},"Payments (Stripe, Telegram Stars)",[96,20104,15861],{"align":12854},[96,20106,20107],{"align":12854},"Transaction status",[81,20109,20110,20113,20116],{},[96,20111,20112],{"align":12854},"Google Calendar \u002F Sheets",[96,20114,20115],{"align":12854},"Bookings, leads",[96,20117,20118],{"align":12854},"Available slots",[11,20120,20121,20122,507],{},"If an integration is in-house (your own CRM or ERP), attach the API description or a contact who owns it. How a bot-to-CRM pipeline works technically is in my ",[38,20123,20124],{"href":19324},"hands-on guide with code",[15,20126,20128],{"id":20127},"_5-roles-and-permissions","5. Roles and permissions",[298,20130,20131,20138,20145],{},[301,20132,20133,20134,20137],{},"Who ",[26,20135,20136],{},"administers"," the bot (edits texts, views leads)?",[301,20139,20140,20141,20144],{},"Who receives ",[26,20142,20143],{},"notifications"," about new leads and payments?",[301,20146,20147,20148,20151],{},"Is ",[26,20149,20150],{},"human handover"," needed, and who is that human during\u002Foutside business hours?",[15,20153,20155],{"id":20154},"_6-non-functional-requirements","6. Non-functional requirements",[11,20157,20158],{},"Short, but explicit:",[298,20160,20161,20166,20172,20178],{},[301,20162,20163,20165],{},[26,20164,19615],{},": how many users per day do you expect (10? 1,000? 10,000?) — the architecture depends on it.",[301,20167,20168,20171],{},[26,20169,20170],{},"Languages",": one or several.",[301,20173,20174,20177],{},[26,20175,20176],{},"Data",": where personal client data lives, whether exports are needed.",[301,20179,20180,20183,20184,20187],{},[26,20181,20182],{},"Platform limits",": the ",[38,20185,19355],{"href":19353,"rel":20186},[429]," rate-limits message sending — mass broadcasts are designed around those limits.",[15,20189,20191],{"id":20190},"what-happens-to-this-brief-next","What happens to this brief next",[11,20193,20194,20195,20198],{},"The completed checklist is one or two pages of text. From it I prepare a specification with milestones and a fixed estimate (price benchmarks are in ",[38,20196,20197],{"href":19628},"how much a Telegram bot costs","), we approve it, and development runs with visible results every 3–5 days.",[11,20200,20201,20202,20204,20205,507],{},"Ready to describe your task? Send the brief via the ",[38,20203,19661],{"href":12750}," — I'll reply with an estimate within 24 hours. If the scenarios haven't taken shape yet, let's work through them together in a ",[38,20206,19665],{"href":12755},{"title":363,"searchDepth":364,"depth":364,"links":20208},[20209,20210,20211,20212,20213,20214,20215],{"id":19981,"depth":364,"text":19982},{"id":20005,"depth":364,"text":20006},{"id":20021,"depth":364,"text":20022},{"id":20065,"depth":364,"text":20066},{"id":20127,"depth":364,"text":20128},{"id":20154,"depth":364,"text":20155},{"id":20190,"depth":364,"text":20191},"2026-05-24","A Telegram bot spec checklist — dialogue flows, states, error cases, integrations, and non-functional requirements. Cuts both the estimate and the timeline.",[20219,20222,20225,20228],{"q":20220,"a":20221},"Do I need a formal specification, or can I describe the task in my own words?","Your own words are fine. A good brief is dialogue scenarios and a list of real customer questions, not a formal document. Turning it into a specification is my job at the briefing stage.",{"q":20223,"a":20224},"Who writes the final spec — the client or the developer?","The developer, based on your brief. You describe the business task and scenarios; I turn them into a specification with states, integrations, and staged milestones — and we approve it before development starts.",{"q":20226,"a":20227},"How detailed should the description be?","One or two pages is enough: the bot's goal, 3–5 step-by-step dialogue scenarios, a list of integrations, and examples of real customer questions. Detail beyond that rarely speeds up the project.",{"q":20229,"a":20230},"What if requirements change mid-project?","That's normal and expected. I show intermediate results every 3–5 days, so adjustments happen along the way, and larger scope changes become a separate stage so the estimate stays transparent.","\u002Fimages\u002Fblog\u002Fblog_bot_spec_checklist.webp",{},"\u002Fblog\u002Fen\u002Ftelegram-bot-specification-checklist",{"title":19973,"description":20217},"blog\u002Fen\u002Ftelegram-bot-specification-checklist",[12790,19695,20237,20238],"Specification","Process","aAvGoijilYh1ejuKbG_tnK_98bhsAmU9ssbxSaeXFEg",{"id":20241,"title":20242,"body":20243,"date":21582,"description":21583,"extension":376,"faq":21584,"image":21594,"lang":394,"meta":21595,"navigation":396,"path":21596,"published":396,"readTime":1591,"seo":21597,"stem":21598,"tags":21599,"updated":12772,"__hash__":21602},"blog\u002Fblog\u002Fen\u002Ftelegram-bot-crm-integration.md","Why Your CRM Needs a Custom Telegram Bot (HubSpot & AmoCRM Integration)",{"type":8,"value":20244,"toc":21575},[20245,20248,20251,20254,20257,20261,20267,20271,20274,20720,20724,20730,21361,21365,21372,21525,21527,21551,21554,21564,21572],[11,20246,20247],{},"For many modern businesses, messaging apps are the highest-converting customer communication channels. However, managing leads manually in chats is highly inefficient. Sales reps miss messages, data is left unrecorded in spreadsheet files, and lead qualification takes hours of manual back-and-forth.",[11,20249,20250],{},"Integrating a custom Telegram bot with your CRM (such as HubSpot, AmoCRM, or Bitrix24) automates the top of your sales funnel.",[11,20252,20253],{},"A custom bot greets potential clients, asks targeted questions using inline menus, performs basic lead scoring, and instantly pushes structured data into your sales pipeline.",[11,20255,20256],{},"In this technical guide, we will design a qualification workflow, write a lead-scoring algorithm, and implement a complete API synchronization script using Node.js.",[15,20258,20260],{"id":20259},"technical-lead-qualification-pipeline","Technical Lead Qualification Pipeline",[1564,20262,20265],{"className":20263,"code":20264,"language":4815,"meta":363},[4813],"[User starts bot]\n        │\n        v\n[Qualify Question 1: Budget?] ──> [Inline buttons: $1k, $5k, $10k+]\n        │\n        v\n[Qualify Question 2: Stack?] ──> [Inline buttons: Vue\u002FNuxt, React, Python]\n        │\n        v\n[Input Contact] ──> [Telegram sharing buttons (Phone) or Text (Email)]\n        │\n        v\n(Run Lead Scoring Algorithm)\nIF budget = \"$10k+\" THEN Priority = \"HIGH\" ELSE Priority = \"MEDIUM\"\n        │\n        v\n(Call HubSpot API via SDK)\nCreate CRM Contact ──> Create CRM Deal ──> Associate Deal with Contact\n        │\n        v\n[Notify Sales Rep on Slack\u002FTelegram with direct Link to CRM]\n",[49,20266,20264],{"__ignoreMap":363},[15,20268,20270],{"id":20269},"_1-defining-the-lead-scoring-algorithm","1. Defining the Lead Scoring Algorithm",[11,20272,20273],{},"Before pushing data to your CRM, calculate a lead score based on user choices. This helps your sales team prioritize high-value projects.",[1564,20275,20277],{"className":11542,"code":20276,"language":11544,"meta":363,"style":363},"export interface QualificationAnswers {\n  budget: string; \u002F\u002F 'under_2k' | '2k_10k' | 'over_10k'\n  timeline: string; \u002F\u002F 'immediate' | '1_month' | 'planning'\n  projectType: string; \u002F\u002F 'saas' | 'bot' | 'consultation'\n}\n\nexport interface LeadScore {\n  score: number;\n  priority: 'LOW' | 'MEDIUM' | 'HIGH';\n}\n\nexport function calculateLeadScore(answers: QualificationAnswers): LeadScore {\n  let score = 0;\n\n  \u002F\u002F 1. Evaluate Budget\n  if (answers.budget === 'over_10k') score += 50;\n  else if (answers.budget === '2k_10k') score += 30;\n  else score += 10;\n\n  \u002F\u002F 2. Evaluate Timeline urgency\n  if (answers.timeline === 'immediate') score += 30;\n  else if (answers.timeline === '1_month') score += 20;\n  else score += 5;\n\n  \u002F\u002F 3. Evaluate Project Alignment\n  if (answers.projectType === 'saas') score += 20; \u002F\u002F High alignment\n  else if (answers.projectType === 'bot') score += 15;\n  else score += 5;\n\n  let priority: 'LOW' | 'MEDIUM' | 'HIGH' = 'LOW';\n  if (score >= 70) priority = 'HIGH';\n  else if (score >= 40) priority = 'MEDIUM';\n\n  return { score, priority };\n}\n",[49,20278,20279,20290,20305,20319,20333,20337,20341,20352,20363,20386,20390,20394,20420,20434,20438,20443,20466,20490,20503,20507,20512,20532,20554,20567,20571,20576,20599,20621,20633,20637,20662,20684,20705,20709,20716],{"__ignoreMap":363},[1572,20280,20281,20283,20285,20288],{"class":1574,"line":1575},[1572,20282,3790],{"class":11551},[1572,20284,14530],{"class":11551},[1572,20286,20287],{"class":6311}," QualificationAnswers",[1572,20289,11578],{"class":1638},[1572,20291,20292,20295,20297,20299,20302],{"class":1574,"line":364},[1572,20293,20294],{"class":11583},"  budget",[1572,20296,8342],{"class":11551},[1572,20298,11601],{"class":1644},[1572,20300,20301],{"class":1638},"; ",[1572,20303,20304],{"class":11834},"\u002F\u002F 'under_2k' | '2k_10k' | 'over_10k'\n",[1572,20306,20307,20310,20312,20314,20316],{"class":1574,"line":370},[1572,20308,20309],{"class":11583},"  timeline",[1572,20311,8342],{"class":11551},[1572,20313,11601],{"class":1644},[1572,20315,20301],{"class":1638},[1572,20317,20318],{"class":11834},"\u002F\u002F 'immediate' | '1_month' | 'planning'\n",[1572,20320,20321,20324,20326,20328,20330],{"class":1574,"line":1591},[1572,20322,20323],{"class":11583},"  projectType",[1572,20325,8342],{"class":11551},[1572,20327,11601],{"class":1644},[1572,20329,20301],{"class":1638},[1572,20331,20332],{"class":11834},"\u002F\u002F 'saas' | 'bot' | 'consultation'\n",[1572,20334,20335],{"class":1574,"line":1270},[1572,20336,1670],{"class":1638},[1572,20338,20339],{"class":1574,"line":4389},[1572,20340,10309],{"emptyLinePlaceholder":396},[1572,20342,20343,20345,20347,20350],{"class":1574,"line":742},[1572,20344,3790],{"class":11551},[1572,20346,14530],{"class":11551},[1572,20348,20349],{"class":6311}," LeadScore",[1572,20351,11578],{"class":1638},[1572,20353,20354,20357,20359,20361],{"class":1574,"line":10336},[1572,20355,20356],{"class":11583},"  score",[1572,20358,8342],{"class":11551},[1572,20360,11589],{"class":1644},[1572,20362,11563],{"class":1638},[1572,20364,20365,20368,20370,20373,20376,20379,20381,20384],{"class":1574,"line":10342},[1572,20366,20367],{"class":11583},"  priority",[1572,20369,8342],{"class":11551},[1572,20371,20372],{"class":1651}," 'LOW'",[1572,20374,20375],{"class":11551}," |",[1572,20377,20378],{"class":1651}," 'MEDIUM'",[1572,20380,20375],{"class":11551},[1572,20382,20383],{"class":1651}," 'HIGH'",[1572,20385,11563],{"class":1638},[1572,20387,20388],{"class":1574,"line":10348},[1572,20389,1670],{"class":1638},[1572,20391,20392],{"class":1574,"line":11656},[1572,20393,10309],{"emptyLinePlaceholder":396},[1572,20395,20396,20398,20400,20403,20405,20408,20410,20412,20414,20416,20418],{"class":1574,"line":11661},[1572,20397,3790],{"class":11551},[1572,20399,7934],{"class":11551},[1572,20401,20402],{"class":6311}," calculateLeadScore",[1572,20404,11715],{"class":1638},[1572,20406,20407],{"class":11583},"answers",[1572,20409,8342],{"class":11551},[1572,20411,20287],{"class":6311},[1572,20413,219],{"class":1638},[1572,20415,8342],{"class":11551},[1572,20417,20349],{"class":6311},[1572,20419,11578],{"class":1638},[1572,20421,20422,20425,20428,20430,20432],{"class":1574,"line":11671},[1572,20423,20424],{"class":11551},"  let",[1572,20426,20427],{"class":1638}," score ",[1572,20429,6315],{"class":11551},[1572,20431,18524],{"class":1644},[1572,20433,11563],{"class":1638},[1572,20435,20436],{"class":1574,"line":11683},[1572,20437,10309],{"emptyLinePlaceholder":396},[1572,20439,20440],{"class":1574,"line":11695},[1572,20441,20442],{"class":11834},"  \u002F\u002F 1. Evaluate Budget\n",[1572,20444,20445,20447,20450,20452,20455,20458,20461,20464],{"class":1574,"line":11700},[1572,20446,11796],{"class":11551},[1572,20448,20449],{"class":1638}," (answers.budget ",[1572,20451,12132],{"class":11551},[1572,20453,20454],{"class":1651}," 'over_10k'",[1572,20456,20457],{"class":1638},") score ",[1572,20459,20460],{"class":11551},"+=",[1572,20462,20463],{"class":1644}," 50",[1572,20465,11563],{"class":1638},[1572,20467,20468,20471,20474,20476,20478,20481,20483,20485,20488],{"class":1574,"line":11705},[1572,20469,20470],{"class":11551},"  else",[1572,20472,20473],{"class":11551}," if",[1572,20475,20449],{"class":1638},[1572,20477,12132],{"class":11551},[1572,20479,20480],{"class":1651}," '2k_10k'",[1572,20482,20457],{"class":1638},[1572,20484,20460],{"class":11551},[1572,20486,20487],{"class":1644}," 30",[1572,20489,11563],{"class":1638},[1572,20491,20492,20494,20496,20498,20501],{"class":1574,"line":11742},[1572,20493,20470],{"class":11551},[1572,20495,20427],{"class":1638},[1572,20497,20460],{"class":11551},[1572,20499,20500],{"class":1644}," 10",[1572,20502,11563],{"class":1638},[1572,20504,20505],{"class":1574,"line":11763},[1572,20506,10309],{"emptyLinePlaceholder":396},[1572,20508,20509],{"class":1574,"line":11787},[1572,20510,20511],{"class":11834},"  \u002F\u002F 2. Evaluate Timeline urgency\n",[1572,20513,20514,20516,20519,20521,20524,20526,20528,20530],{"class":1574,"line":11793},[1572,20515,11796],{"class":11551},[1572,20517,20518],{"class":1638}," (answers.timeline ",[1572,20520,12132],{"class":11551},[1572,20522,20523],{"class":1651}," 'immediate'",[1572,20525,20457],{"class":1638},[1572,20527,20460],{"class":11551},[1572,20529,20487],{"class":1644},[1572,20531,11563],{"class":1638},[1572,20533,20534,20536,20538,20540,20542,20545,20547,20549,20552],{"class":1574,"line":11806},[1572,20535,20470],{"class":11551},[1572,20537,20473],{"class":11551},[1572,20539,20518],{"class":1638},[1572,20541,12132],{"class":11551},[1572,20543,20544],{"class":1651}," '1_month'",[1572,20546,20457],{"class":1638},[1572,20548,20460],{"class":11551},[1572,20550,20551],{"class":1644}," 20",[1572,20553,11563],{"class":1638},[1572,20555,20556,20558,20560,20562,20565],{"class":1574,"line":11820},[1572,20557,20470],{"class":11551},[1572,20559,20427],{"class":1638},[1572,20561,20460],{"class":11551},[1572,20563,20564],{"class":1644}," 5",[1572,20566,11563],{"class":1638},[1572,20568,20569],{"class":1574,"line":11826},[1572,20570,10309],{"emptyLinePlaceholder":396},[1572,20572,20573],{"class":1574,"line":11831},[1572,20574,20575],{"class":11834},"  \u002F\u002F 3. Evaluate Project Alignment\n",[1572,20577,20578,20580,20583,20585,20588,20590,20592,20594,20596],{"class":1574,"line":11838},[1572,20579,11796],{"class":11551},[1572,20581,20582],{"class":1638}," (answers.projectType ",[1572,20584,12132],{"class":11551},[1572,20586,20587],{"class":1651}," 'saas'",[1572,20589,20457],{"class":1638},[1572,20591,20460],{"class":11551},[1572,20593,20551],{"class":1644},[1572,20595,20301],{"class":1638},[1572,20597,20598],{"class":11834},"\u002F\u002F High alignment\n",[1572,20600,20601,20603,20605,20607,20609,20612,20614,20616,20619],{"class":1574,"line":11890},[1572,20602,20470],{"class":11551},[1572,20604,20473],{"class":11551},[1572,20606,20582],{"class":1638},[1572,20608,12132],{"class":11551},[1572,20610,20611],{"class":1651}," 'bot'",[1572,20613,20457],{"class":1638},[1572,20615,20460],{"class":11551},[1572,20617,20618],{"class":1644}," 15",[1572,20620,11563],{"class":1638},[1572,20622,20623,20625,20627,20629,20631],{"class":1574,"line":11895},[1572,20624,20470],{"class":11551},[1572,20626,20427],{"class":1638},[1572,20628,20460],{"class":11551},[1572,20630,20564],{"class":1644},[1572,20632,11563],{"class":1638},[1572,20634,20635],{"class":1574,"line":11901},[1572,20636,10309],{"emptyLinePlaceholder":396},[1572,20638,20639,20641,20644,20646,20648,20650,20652,20654,20656,20658,20660],{"class":1574,"line":11914},[1572,20640,20424],{"class":11551},[1572,20642,20643],{"class":1638}," priority",[1572,20645,8342],{"class":11551},[1572,20647,20372],{"class":1651},[1572,20649,20375],{"class":11551},[1572,20651,20378],{"class":1651},[1572,20653,20375],{"class":11551},[1572,20655,20383],{"class":1651},[1572,20657,11751],{"class":11551},[1572,20659,20372],{"class":1651},[1572,20661,11563],{"class":1638},[1572,20663,20664,20666,20669,20672,20675,20678,20680,20682],{"class":1574,"line":11956},[1572,20665,11796],{"class":11551},[1572,20667,20668],{"class":1638}," (score ",[1572,20670,20671],{"class":11551},">=",[1572,20673,20674],{"class":1644}," 70",[1572,20676,20677],{"class":1638},") priority ",[1572,20679,6315],{"class":11551},[1572,20681,20383],{"class":1651},[1572,20683,11563],{"class":1638},[1572,20685,20686,20688,20690,20692,20694,20697,20699,20701,20703],{"class":1574,"line":11976},[1572,20687,20470],{"class":11551},[1572,20689,20473],{"class":11551},[1572,20691,20668],{"class":1638},[1572,20693,20671],{"class":11551},[1572,20695,20696],{"class":1644}," 40",[1572,20698,20677],{"class":1638},[1572,20700,6315],{"class":11551},[1572,20702,20378],{"class":1651},[1572,20704,11563],{"class":1638},[1572,20706,20707],{"class":1574,"line":11981},[1572,20708,10309],{"emptyLinePlaceholder":396},[1572,20710,20711,20713],{"class":1574,"line":11987},[1572,20712,12639],{"class":11551},[1572,20714,20715],{"class":1638}," { score, priority };\n",[1572,20717,20718],{"class":1574,"line":11993},[1572,20719,1670],{"class":1638},[15,20721,20723],{"id":20722},"_2-syncing-leads-with-hubspot-crm-api","2. Syncing Leads with HubSpot CRM API",[11,20725,20726,20727,507],{},"Here is a robust implementation using TypeScript and Axios to create a verified contact and a prioritized deal in ",[26,20728,20729],{},"HubSpot",[1564,20731,20733],{"className":11542,"code":20732,"language":11544,"meta":363,"style":363},"import axios from 'axios';\n\ninterface LeadPayload {\n  firstName: string;\n  lastName?: string;\n  phone: string;\n  telegramUsername?: string;\n  score: LeadScore;\n  answers: QualificationAnswers;\n}\n\nexport async function createHubSpotLead(lead: LeadPayload) {\n  const token = process.env.HUBSPOT_ACCESS_TOKEN;\n  const headers = {\n    Authorization: `Bearer ${token}`,\n    'Content-Type': 'application\u002Fjson',\n  };\n\n  try {\n    \u002F\u002F 1. Create HubSpot Contact\n    const contactRes = await axios.post(\n      'https:\u002F\u002Fapi.hubapi.com\u002Fcrm\u002Fv3\u002Fobjects\u002Fcontacts',\n      {\n        properties: {\n          firstname: lead.firstName,\n          lastname: lead.lastName || '',\n          phone: lead.phone,\n          telegram_username: lead.telegramUsername ? `@${lead.telegramUsername}` : 'N\u002FA',\n          hs_lead_status: 'NEW',\n        },\n      },\n      { headers }\n    );\n\n    const contactId = contactRes.data.id;\n\n    \u002F\u002F 2. Create HubSpot Deal with custom priority tags\n    const dealRes = await axios.post(\n      'https:\u002F\u002Fapi.hubapi.com\u002Fcrm\u002Fv3\u002Fobjects\u002Fdeals',\n      {\n        properties: {\n          dealname: `Lead: ${lead.firstName} - ${lead.answers.projectType.toUpperCase()}`,\n          dealstage: 'appointmentscheduled',\n          priority: lead.score.priority, \u002F\u002F 'HIGH', 'MEDIUM', or 'LOW'\n          description: `Score: ${lead.score.score}\u002F100. Budget: ${lead.answers.budget}. Timeline: ${lead.answers.timeline}`,\n        },\n      },\n      { headers }\n    );\n\n    const dealId = dealRes.data.id;\n\n    \u002F\u002F 3. Associate Deal with Contact\n    await axios.put(\n      `https:\u002F\u002Fapi.hubapi.com\u002Fcrm\u002Fv3\u002Fobjects\u002Fdeals\u002F${dealId}\u002Fassociations\u002Fcontacts\u002F${contactId}\u002Fdeal_to_contact`,\n      {},\n      { headers }\n    );\n\n    return { contactId, dealId };\n  } catch (error: any) {\n    console.error('[CRM Sync Error] HubSpot registration failed:', error.response?.data || error.message);\n    throw new Error('CRM Sync Failed');\n  }\n}\n",[49,20734,20735,20749,20753,20762,20773,20784,20795,20806,20816,20827,20831,20835,20857,20873,20884,20899,20910,20915,20919,20925,20930,20948,20955,20959,20964,20969,20980,20985,21012,21022,21027,21031,21036,21041,21045,21057,21061,21066,21083,21090,21094,21098,21139,21149,21157,21208,21212,21216,21220,21224,21228,21240,21244,21249,21260,21279,21284,21288,21292,21296,21303,21319,21338,21353,21357],{"__ignoreMap":363},[1572,20736,20737,20739,20742,20744,20747],{"class":1574,"line":1575},[1572,20738,5892],{"class":11551},[1572,20740,20741],{"class":1638}," axios ",[1572,20743,11557],{"class":11551},[1572,20745,20746],{"class":1651}," 'axios'",[1572,20748,11563],{"class":1638},[1572,20750,20751],{"class":1574,"line":364},[1572,20752,10309],{"emptyLinePlaceholder":396},[1572,20754,20755,20757,20760],{"class":1574,"line":370},[1572,20756,11572],{"class":11551},[1572,20758,20759],{"class":6311}," LeadPayload",[1572,20761,11578],{"class":1638},[1572,20763,20764,20767,20769,20771],{"class":1574,"line":1591},[1572,20765,20766],{"class":11583},"  firstName",[1572,20768,8342],{"class":11551},[1572,20770,11601],{"class":1644},[1572,20772,11563],{"class":1638},[1572,20774,20775,20778,20780,20782],{"class":1574,"line":1270},[1572,20776,20777],{"class":11583},"  lastName",[1572,20779,11611],{"class":11551},[1572,20781,11601],{"class":1644},[1572,20783,11563],{"class":1638},[1572,20785,20786,20789,20791,20793],{"class":1574,"line":4389},[1572,20787,20788],{"class":11583},"  phone",[1572,20790,8342],{"class":11551},[1572,20792,11601],{"class":1644},[1572,20794,11563],{"class":1638},[1572,20796,20797,20800,20802,20804],{"class":1574,"line":742},[1572,20798,20799],{"class":11583},"  telegramUsername",[1572,20801,11611],{"class":11551},[1572,20803,11601],{"class":1644},[1572,20805,11563],{"class":1638},[1572,20807,20808,20810,20812,20814],{"class":1574,"line":10336},[1572,20809,20356],{"class":11583},[1572,20811,8342],{"class":11551},[1572,20813,20349],{"class":6311},[1572,20815,11563],{"class":1638},[1572,20817,20818,20821,20823,20825],{"class":1574,"line":10342},[1572,20819,20820],{"class":11583},"  answers",[1572,20822,8342],{"class":11551},[1572,20824,20287],{"class":6311},[1572,20826,11563],{"class":1638},[1572,20828,20829],{"class":1574,"line":10348},[1572,20830,1670],{"class":1638},[1572,20832,20833],{"class":1574,"line":11656},[1572,20834,10309],{"emptyLinePlaceholder":396},[1572,20836,20837,20839,20841,20843,20846,20848,20851,20853,20855],{"class":1574,"line":11661},[1572,20838,3790],{"class":11551},[1572,20840,14583],{"class":11551},[1572,20842,7934],{"class":11551},[1572,20844,20845],{"class":6311}," createHubSpotLead",[1572,20847,11715],{"class":1638},[1572,20849,20850],{"class":11583},"lead",[1572,20852,8342],{"class":11551},[1572,20854,20759],{"class":6311},[1572,20856,15641],{"class":1638},[1572,20858,20859,20861,20864,20866,20868,20871],{"class":1574,"line":11671},[1572,20860,11745],{"class":11551},[1572,20862,20863],{"class":1644}," token",[1572,20865,11751],{"class":11551},[1572,20867,15405],{"class":1638},[1572,20869,20870],{"class":1644},"HUBSPOT_ACCESS_TOKEN",[1572,20872,11563],{"class":1638},[1572,20874,20875,20877,20880,20882],{"class":1574,"line":11683},[1572,20876,11745],{"class":11551},[1572,20878,20879],{"class":1644}," headers",[1572,20881,11751],{"class":11551},[1572,20883,11578],{"class":1638},[1572,20885,20886,20889,20892,20895,20897],{"class":1574,"line":11695},[1572,20887,20888],{"class":1638},"    Authorization: ",[1572,20890,20891],{"class":1651},"`Bearer ${",[1572,20893,20894],{"class":1638},"token",[1572,20896,11950],{"class":1651},[1572,20898,1655],{"class":1638},[1572,20900,20901,20904,20906,20908],{"class":1574,"line":11700},[1572,20902,20903],{"class":1651},"    'Content-Type'",[1572,20905,1648],{"class":1638},[1572,20907,15458],{"class":1651},[1572,20909,1655],{"class":1638},[1572,20911,20912],{"class":1574,"line":11705},[1572,20913,20914],{"class":1638},"  };\n",[1572,20916,20917],{"class":1574,"line":11742},[1572,20918,10309],{"emptyLinePlaceholder":396},[1572,20920,20921,20923],{"class":1574,"line":11763},[1572,20922,12185],{"class":11551},[1572,20924,11578],{"class":1638},[1572,20926,20927],{"class":1574,"line":11787},[1572,20928,20929],{"class":11834},"    \u002F\u002F 1. Create HubSpot Contact\n",[1572,20931,20932,20934,20937,20939,20941,20944,20946],{"class":1574,"line":11793},[1572,20933,12193],{"class":11551},[1572,20935,20936],{"class":1644}," contactRes",[1572,20938,11751],{"class":11551},[1572,20940,14623],{"class":11551},[1572,20942,20943],{"class":1638}," axios.",[1572,20945,15434],{"class":6311},[1572,20947,15437],{"class":1638},[1572,20949,20950,20953],{"class":1574,"line":11806},[1572,20951,20952],{"class":1651},"      'https:\u002F\u002Fapi.hubapi.com\u002Fcrm\u002Fv3\u002Fobjects\u002Fcontacts'",[1572,20954,1655],{"class":1638},[1572,20956,20957],{"class":1574,"line":11820},[1572,20958,14652],{"class":1638},[1572,20960,20961],{"class":1574,"line":11826},[1572,20962,20963],{"class":1638},"        properties: {\n",[1572,20965,20966],{"class":1574,"line":11831},[1572,20967,20968],{"class":1638},"          firstname: lead.firstName,\n",[1572,20970,20971,20974,20976,20978],{"class":1574,"line":11838},[1572,20972,20973],{"class":1638},"          lastname: lead.lastName ",[1572,20975,4834],{"class":11551},[1572,20977,18533],{"class":1651},[1572,20979,1655],{"class":1638},[1572,20981,20982],{"class":1574,"line":11890},[1572,20983,20984],{"class":1638},"          phone: lead.phone,\n",[1572,20986,20987,20990,20992,20995,20997,20999,21002,21004,21007,21010],{"class":1574,"line":11895},[1572,20988,20989],{"class":1638},"          telegram_username: lead.telegramUsername ",[1572,20991,6735],{"class":11551},[1572,20993,20994],{"class":1651}," `@${",[1572,20996,20850],{"class":1638},[1572,20998,507],{"class":1651},[1572,21000,21001],{"class":1638},"telegramUsername",[1572,21003,11950],{"class":1651},[1572,21005,21006],{"class":11551}," :",[1572,21008,21009],{"class":1651}," 'N\u002FA'",[1572,21011,1655],{"class":1638},[1572,21013,21014,21017,21020],{"class":1574,"line":11901},[1572,21015,21016],{"class":1638},"          hs_lead_status: ",[1572,21018,21019],{"class":1651},"'NEW'",[1572,21021,1655],{"class":1638},[1572,21023,21024],{"class":1574,"line":11914},[1572,21025,21026],{"class":1638},"        },\n",[1572,21028,21029],{"class":1574,"line":11956},[1572,21030,13649],{"class":1638},[1572,21032,21033],{"class":1574,"line":11976},[1572,21034,21035],{"class":1638},"      { headers }\n",[1572,21037,21038],{"class":1574,"line":11981},[1572,21039,21040],{"class":1638},"    );\n",[1572,21042,21043],{"class":1574,"line":11987},[1572,21044,10309],{"emptyLinePlaceholder":396},[1572,21046,21047,21049,21052,21054],{"class":1574,"line":11993},[1572,21048,12193],{"class":11551},[1572,21050,21051],{"class":1644}," contactId",[1572,21053,11751],{"class":11551},[1572,21055,21056],{"class":1638}," contactRes.data.id;\n",[1572,21058,21059],{"class":1574,"line":12006},[1572,21060,10309],{"emptyLinePlaceholder":396},[1572,21062,21063],{"class":1574,"line":12026},[1572,21064,21065],{"class":11834},"    \u002F\u002F 2. Create HubSpot Deal with custom priority tags\n",[1572,21067,21068,21070,21073,21075,21077,21079,21081],{"class":1574,"line":12037},[1572,21069,12193],{"class":11551},[1572,21071,21072],{"class":1644}," dealRes",[1572,21074,11751],{"class":11551},[1572,21076,14623],{"class":11551},[1572,21078,20943],{"class":1638},[1572,21080,15434],{"class":6311},[1572,21082,15437],{"class":1638},[1572,21084,21085,21088],{"class":1574,"line":12047},[1572,21086,21087],{"class":1651},"      'https:\u002F\u002Fapi.hubapi.com\u002Fcrm\u002Fv3\u002Fobjects\u002Fdeals'",[1572,21089,1655],{"class":1638},[1572,21091,21092],{"class":1574,"line":12052},[1572,21093,14652],{"class":1638},[1572,21095,21096],{"class":1574,"line":12058},[1572,21097,20963],{"class":1638},[1572,21099,21100,21103,21106,21108,21110,21113,21116,21118,21120,21122,21124,21127,21129,21132,21135,21137],{"class":1574,"line":12070},[1572,21101,21102],{"class":1638},"          dealname: ",[1572,21104,21105],{"class":1651},"`Lead: ${",[1572,21107,20850],{"class":1638},[1572,21109,507],{"class":1651},[1572,21111,21112],{"class":1638},"firstName",[1572,21114,21115],{"class":1651},"} - ${",[1572,21117,20850],{"class":1638},[1572,21119,507],{"class":1651},[1572,21121,20407],{"class":1638},[1572,21123,507],{"class":1651},[1572,21125,21126],{"class":1638},"projectType",[1572,21128,507],{"class":1651},[1572,21130,21131],{"class":6311},"toUpperCase",[1572,21133,21134],{"class":1651},"()",[1572,21136,11950],{"class":1651},[1572,21138,1655],{"class":1638},[1572,21140,21141,21144,21147],{"class":1574,"line":12084},[1572,21142,21143],{"class":1638},"          dealstage: ",[1572,21145,21146],{"class":1651},"'appointmentscheduled'",[1572,21148,1655],{"class":1638},[1572,21150,21151,21154],{"class":1574,"line":12094},[1572,21152,21153],{"class":1638},"          priority: lead.score.priority, ",[1572,21155,21156],{"class":11834},"\u002F\u002F 'HIGH', 'MEDIUM', or 'LOW'\n",[1572,21158,21159,21162,21165,21167,21169,21172,21174,21176,21179,21181,21183,21185,21187,21190,21193,21195,21197,21199,21201,21204,21206],{"class":1574,"line":12108},[1572,21160,21161],{"class":1638},"          description: ",[1572,21163,21164],{"class":1651},"`Score: ${",[1572,21166,20850],{"class":1638},[1572,21168,507],{"class":1651},[1572,21170,21171],{"class":1638},"score",[1572,21173,507],{"class":1651},[1572,21175,21171],{"class":1638},[1572,21177,21178],{"class":1651},"}\u002F100. Budget: ${",[1572,21180,20850],{"class":1638},[1572,21182,507],{"class":1651},[1572,21184,20407],{"class":1638},[1572,21186,507],{"class":1651},[1572,21188,21189],{"class":1638},"budget",[1572,21191,21192],{"class":1651},"}. Timeline: ${",[1572,21194,20850],{"class":1638},[1572,21196,507],{"class":1651},[1572,21198,20407],{"class":1638},[1572,21200,507],{"class":1651},[1572,21202,21203],{"class":1638},"timeline",[1572,21205,11950],{"class":1651},[1572,21207,1655],{"class":1638},[1572,21209,21210],{"class":1574,"line":12113},[1572,21211,21026],{"class":1638},[1572,21213,21214],{"class":1574,"line":12119},[1572,21215,13649],{"class":1638},[1572,21217,21218],{"class":1574,"line":12138},[1572,21219,21035],{"class":1638},[1572,21221,21222],{"class":1574,"line":12143},[1572,21223,21040],{"class":1638},[1572,21225,21226],{"class":1574,"line":12155},[1572,21227,10309],{"emptyLinePlaceholder":396},[1572,21229,21230,21232,21235,21237],{"class":1574,"line":12166},[1572,21231,12193],{"class":11551},[1572,21233,21234],{"class":1644}," dealId",[1572,21236,11751],{"class":11551},[1572,21238,21239],{"class":1638}," dealRes.data.id;\n",[1572,21241,21242],{"class":1574,"line":12171},[1572,21243,10309],{"emptyLinePlaceholder":396},[1572,21245,21246],{"class":1574,"line":12176},[1572,21247,21248],{"class":11834},"    \u002F\u002F 3. Associate Deal with Contact\n",[1572,21250,21251,21253,21255,21258],{"class":1574,"line":12182},[1572,21252,17028],{"class":11551},[1572,21254,20943],{"class":1638},[1572,21256,21257],{"class":6311},"put",[1572,21259,15437],{"class":1638},[1572,21261,21262,21265,21268,21271,21274,21277],{"class":1574,"line":12190},[1572,21263,21264],{"class":1651},"      `https:\u002F\u002Fapi.hubapi.com\u002Fcrm\u002Fv3\u002Fobjects\u002Fdeals\u002F${",[1572,21266,21267],{"class":1638},"dealId",[1572,21269,21270],{"class":1651},"}\u002Fassociations\u002Fcontacts\u002F${",[1572,21272,21273],{"class":1638},"contactId",[1572,21275,21276],{"class":1651},"}\u002Fdeal_to_contact`",[1572,21278,1655],{"class":1638},[1572,21280,21281],{"class":1574,"line":12212},[1572,21282,21283],{"class":1638},"      {},\n",[1572,21285,21286],{"class":1574,"line":12249},[1572,21287,21035],{"class":1638},[1572,21289,21290],{"class":1574,"line":12261},[1572,21291,21040],{"class":1638},[1572,21293,21294],{"class":1574,"line":12273},[1572,21295,10309],{"emptyLinePlaceholder":396},[1572,21297,21298,21300],{"class":1574,"line":12284},[1572,21299,11809],{"class":11551},[1572,21301,21302],{"class":1638}," { contactId, dealId };\n",[1572,21304,21305,21307,21309,21311,21313,21315,21317],{"class":1574,"line":12289},[1572,21306,12264],{"class":1638},[1572,21308,12267],{"class":11551},[1572,21310,4110],{"class":1638},[1572,21312,15649],{"class":11583},[1572,21314,8342],{"class":11551},[1572,21316,12479],{"class":1644},[1572,21318,15641],{"class":1638},[1572,21320,21321,21323,21325,21327,21330,21333,21335],{"class":1574,"line":16012},[1572,21322,19264],{"class":1638},[1572,21324,15649],{"class":6311},[1572,21326,11715],{"class":1638},[1572,21328,21329],{"class":1651},"'[CRM Sync Error] HubSpot registration failed:'",[1572,21331,21332],{"class":1638},", error.response?.data ",[1572,21334,4834],{"class":11551},[1572,21336,21337],{"class":1638}," error.message);\n",[1572,21339,21340,21342,21344,21346,21348,21351],{"class":1574,"line":16017},[1572,21341,14776],{"class":11551},[1572,21343,11754],{"class":11551},[1572,21345,14781],{"class":6311},[1572,21347,11715],{"class":1638},[1572,21349,21350],{"class":1651},"'CRM Sync Failed'",[1572,21352,11784],{"class":1638},[1572,21354,21355],{"class":1574,"line":16042},[1572,21356,11823],{"class":1638},[1572,21358,21359],{"class":1574,"line":16055},[1572,21360,1670],{"class":1638},[15,21362,21364],{"id":21363},"_3-designing-interactive-telegram-inline-controls","3. Designing Interactive Telegram Inline Controls",[11,21366,21367,21368,21371],{},"To ensure high conversions, avoid text inputs where possible. Rely on ",[26,21369,21370],{},"Inline Keyboards"," for selecting options. Here is a sample helper for sending qualification questions:",[1564,21373,21375],{"className":11542,"code":21374,"language":11544,"meta":363,"style":363},"import TelegramBot from 'node-telegram-bot-api';\n\nexport function sendBudgetQuestion(bot: TelegramBot, chatId: number) {\n  bot.sendMessage(chatId, \"What is your approximate budget for the project?\", {\n    reply_markup: {\n      inline_keyboard: [\n        [\n          { text: \"Under $2k\", callback_data: \"budget_under_2k\" },\n          { text: \"$2k - $10k\", callback_data: \"budget_2k_10k\" }\n        ],\n        [\n          { text: \"Over $10k\", callback_data: \"budget_over_10k\" }\n        ]\n      ]\n    }\n  });\n}\n",[49,21376,21377,21389,21393,21422,21436,21441,21446,21451,21467,21481,21486,21490,21504,21509,21513,21517,21521],{"__ignoreMap":363},[1572,21378,21379,21381,21383,21385,21387],{"class":1574,"line":1575},[1572,21380,5892],{"class":11551},[1572,21382,18886],{"class":1638},[1572,21384,11557],{"class":11551},[1572,21386,18891],{"class":1651},[1572,21388,11563],{"class":1638},[1572,21390,21391],{"class":1574,"line":364},[1572,21392,10309],{"emptyLinePlaceholder":396},[1572,21394,21395,21397,21399,21402,21404,21407,21409,21411,21413,21416,21418,21420],{"class":1574,"line":370},[1572,21396,3790],{"class":11551},[1572,21398,7934],{"class":11551},[1572,21400,21401],{"class":6311}," sendBudgetQuestion",[1572,21403,11715],{"class":1638},[1572,21405,21406],{"class":11583},"bot",[1572,21408,8342],{"class":11551},[1572,21410,18911],{"class":6311},[1572,21412,673],{"class":1638},[1572,21414,21415],{"class":11583},"chatId",[1572,21417,8342],{"class":11551},[1572,21419,11589],{"class":1644},[1572,21421,15641],{"class":1638},[1572,21423,21424,21427,21429,21431,21434],{"class":1574,"line":1591},[1572,21425,21426],{"class":1638},"  bot.",[1572,21428,19200],{"class":6311},[1572,21430,19285],{"class":1638},[1572,21432,21433],{"class":1651},"\"What is your approximate budget for the project?\"",[1572,21435,14505],{"class":1638},[1572,21437,21438],{"class":1574,"line":1270},[1572,21439,21440],{"class":1638},"    reply_markup: {\n",[1572,21442,21443],{"class":1574,"line":4389},[1572,21444,21445],{"class":1638},"      inline_keyboard: [\n",[1572,21447,21448],{"class":1574,"line":742},[1572,21449,21450],{"class":1638},"        [\n",[1572,21452,21453,21456,21459,21462,21465],{"class":1574,"line":10336},[1572,21454,21455],{"class":1638},"          { text: ",[1572,21457,21458],{"class":1651},"\"Under $2k\"",[1572,21460,21461],{"class":1638},", callback_data: ",[1572,21463,21464],{"class":1651},"\"budget_under_2k\"",[1572,21466,13858],{"class":1638},[1572,21468,21469,21471,21474,21476,21479],{"class":1574,"line":10342},[1572,21470,21455],{"class":1638},[1572,21472,21473],{"class":1651},"\"$2k - $10k\"",[1572,21475,21461],{"class":1638},[1572,21477,21478],{"class":1651},"\"budget_2k_10k\"",[1572,21480,12347],{"class":1638},[1572,21482,21483],{"class":1574,"line":10348},[1572,21484,21485],{"class":1638},"        ],\n",[1572,21487,21488],{"class":1574,"line":11656},[1572,21489,21450],{"class":1638},[1572,21491,21492,21494,21497,21499,21502],{"class":1574,"line":11661},[1572,21493,21455],{"class":1638},[1572,21495,21496],{"class":1651},"\"Over $10k\"",[1572,21498,21461],{"class":1638},[1572,21500,21501],{"class":1651},"\"budget_over_10k\"",[1572,21503,12347],{"class":1638},[1572,21505,21506],{"class":1574,"line":11671},[1572,21507,21508],{"class":1638},"        ]\n",[1572,21510,21511],{"class":1574,"line":11683},[1572,21512,13573],{"class":1638},[1572,21514,21515],{"class":1574,"line":11695},[1572,21516,12625],{"class":1638},[1572,21518,21519],{"class":1574,"line":11700},[1572,21520,12630],{"class":1638},[1572,21522,21523],{"class":1574,"line":11705},[1572,21524,1670],{"class":1638},[15,21526,12723],{"id":12722},[298,21528,21529,21535,21543],{},[301,21530,21531,21534],{},[38,21532,19355],{"href":19353,"rel":21533},[429]," — inline keyboards, callback queries, and contact sharing",[301,21536,21537,21542],{},[38,21538,21541],{"href":21539,"rel":21540},"https:\u002F\u002Fdevelopers.hubspot.com\u002Fdocs\u002Fapi\u002Foverview",[429],"HubSpot API documentation"," — contact and deal endpoints",[301,21544,21545,21550],{},[38,21546,21549],{"href":21547,"rel":21548},"https:\u002F\u002Fwww.amocrm.com\u002Fdevelopers\u002F",[429],"amoCRM developer portal"," — the API used in the example above",[11,21552,21553],{},"By verifying user data, implementing scoring, and establishing direct CRM pipelines, businesses can reduce lead response times to under 5 seconds while ensuring that managers spend time talking only to qualified clients.",[11,21555,21556,21557,21559,21560,21563],{},"In my custom bot projects like ",[38,21558,12706],{"href":12705},", we leverage these webhook workflows to sync user activities directly into sales tools. For an even more advanced system, you can connect your CRM database with a ",[38,21561,21562],{"href":12710},"RAG AI Customer Support Bot"," to resolve user issues before a manager is paged.",[11,21565,21566,21567,13330,21569,21571],{},"If you are looking to build a custom Telegram bot with advanced CRM integrations (amoCRM, Bitrix24, HubSpot) tailored for your sales processes, explore my ",[38,21568,12751],{"href":12750},[38,21570,13333],{"href":12755}," to plan your funnel.",[1779,21573,21574],{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":363,"searchDepth":364,"depth":364,"links":21576},[21577,21578,21579,21580,21581],{"id":20259,"depth":364,"text":20260},{"id":20269,"depth":364,"text":20270},{"id":20722,"depth":364,"text":20723},{"id":21363,"depth":364,"text":21364},{"id":12722,"depth":364,"text":12723},"2026-05-21","A complete developer's guide on constructing an interactive qualification Telegram bot that processes lead inputs, runs scoring algorithms, and syncs pipelines with CRM APIs.",[21585,21588,21591],{"q":21586,"a":21587},"Why does a bot need lead scoring if the CRM already collects leads?","Scoring sorts leads before a manager sees them: a lead with a $10,000+ budget and an urgent timeline lands in the CRM with high priority and an instant notification, while cold leads don't distract the sales team. The most profitable clients get answered first.",{"q":21589,"a":21590},"Which CRMs can a Telegram bot integrate with?","Any system with an API: HubSpot, amoCRM, Bitrix24, as well as in-house CRMs. The bot creates a contact, opens a deal at the right pipeline stage, and maps survey answers to custom fields.",{"q":21592,"a":21593},"Why are buttons better than free-text input in a bot?","Inline buttons raise survey completion rates: the user doesn't have to type, and the bot receives predictable data without parsing free text. Manual input stays only where unavoidable — phone number or email.","\u002Fimages\u002Fblog\u002Fblog_crm_integration.webp",{},"\u002Fblog\u002Fen\u002Ftelegram-bot-crm-integration",{"title":20242,"description":21583},"blog\u002Fen\u002Ftelegram-bot-crm-integration",[12790,21600,16578,12793,21601],"CRM","Automation","34J8RtBVVlAujFnvSPvM1ZMVTZiURTqQ622zXJtQsxI",{"id":21604,"title":21605,"body":21606,"date":21859,"description":21860,"extension":376,"faq":405,"image":21649,"lang":394,"meta":21861,"navigation":396,"path":21862,"published":396,"readTime":364,"seo":21863,"stem":21864,"tags":21865,"updated":12772,"__hash__":21873},"blog\u002Fblog\u002Fen\u002Fezswitch-macos-switcher.md","EzSwitch — Quick Keyboard Layout Switcher for macOS",{"type":8,"value":21607,"toc":21847},[21608,21618,21631,21634,21638,21641,21645,21651,21661,21676,21680,21690,21698,21702,21705,21709,21715,21761,21768,21772,21779,21785,21789,21796,21823,21827,21835,21845],[11,21609,21610,21611,21613,21614,21617],{},"For developers and writers who constantly switch between multiple languages (e.g., writing code in English and comments or docs in Russian), physical keyboard layout switching is a constant friction point. Accidentally typing in the wrong layout (",[49,21612,6871],{}," instead of ",[49,21615,21616],{},"привет",") breaks the working flow.",[11,21619,21620,21621,21626,21627,21630],{},"To solve this once and for all, I developed ",[38,21622,21625],{"href":21623,"rel":21624},"https:\u002F\u002Fgithub.com\u002Falexrel\u002FEzSwitch",[429],"EzSwitch"," — a lightweight macOS menu bar utility written in ",[26,21628,21629],{},"Swift and SwiftUI"," that automates keyboard layout changes and text recovery.",[11,21632,21633],{},"In this article, I will walk through the features of EzSwitch and explain how the key features operate under the hood using macOS system APIs.",[15,21635,21637],{"id":21636},"core-features-of-ezswitch","Core Features of EzSwitch",[11,21639,21640],{},"The utility solves two major problems: fast layout switching using physical modifier keys, and instant correction of already typed text.",[70,21642,21644],{"id":21643},"_1-command-key-layout-switching","1. Command-Key Layout Switching",[21646,21647],"blog-image",{"alt":21648,"caption":21648,"layout":11348,"src":21649,"size":21650},"EzSwitch interface in the menu bar","\u002Fimages\u002Fezswitch\u002FezSwitch-app.png","sm",[11,21652,21653,21654,698,21657,21660],{},"Instead of utilizing standard macOS cycling combinations like ",[49,21655,21656],{},"Cmd+Space",[49,21658,21659],{},"Ctrl+Space",", EzSwitch lets you bind layouts to individual taps of modifier keys:",[298,21662,21663,21670],{},[301,21664,21665,21666,21669],{},"Tapping the ",[26,21667,21668],{},"left Command"," key ➔ forces the active layout to your left language (e.g., English).",[301,21671,21665,21672,21675],{},[26,21673,21674],{},"right Command"," key ➔ forces the active layout to your right language (e.g., Russian).\nThis is fast and error-free because you do not need to check which layout is currently active. You simply press the side corresponding to your target language.",[70,21677,21679],{"id":21678},"_2-double-shift-text-transformation","2. Double-Shift Text Transformation",[11,21681,21682,21683,21685,21686,21689],{},"If you forget to switch layouts and type a word in the wrong mode, simply highlight the text (or place your cursor after the word) and double-tap ",[26,21684,6136],{}," (or ",[26,21687,21688],{},"Option","). The text instantly transforms into the correct layout.",[298,21691,21692,21695],{},[301,21693,21694],{},"Supports converting both highlighted text selections or the last typed word before the cursor.",[301,21696,21697],{},"Automatically switches the active keyboard layout to follow the transformed text.",[15,21699,21701],{"id":21700},"how-it-works-under-the-hood","How it Works Under the Hood",[11,21703,21704],{},"Building low-level system software for macOS requires deep interaction with Core Graphics and Carbon APIs.",[70,21706,21708],{"id":21707},"intercepting-keystrokes-event-taps","Intercepting Keystrokes (Event Taps)",[11,21710,21711,21712,8342],{},"To detect modifier double-taps and single Command key taps, the application sets up a low-level event listener in Swift using ",[26,21713,21714],{},"CGEventTap",[1564,21716,21720],{"className":21717,"code":21718,"language":21719,"meta":363,"style":363},"language-swift shiki shiki-themes github-dark","let eventTap = CGEvent.tapCreate(\n    tap: .cgSessionEventTap,\n    place: .headInsertEventTap,\n    options: .defaultTap,\n    eventsOfInterest: [.keyDown, .keyUp, .flagsChanged],\n    callback: myEventTapCallback,\n    userInfo: nil\n)\n","swift",[49,21721,21722,21727,21732,21737,21742,21747,21752,21757],{"__ignoreMap":363},[1572,21723,21724],{"class":1574,"line":1575},[1572,21725,21726],{},"let eventTap = CGEvent.tapCreate(\n",[1572,21728,21729],{"class":1574,"line":364},[1572,21730,21731],{},"    tap: .cgSessionEventTap,\n",[1572,21733,21734],{"class":1574,"line":370},[1572,21735,21736],{},"    place: .headInsertEventTap,\n",[1572,21738,21739],{"class":1574,"line":1591},[1572,21740,21741],{},"    options: .defaultTap,\n",[1572,21743,21744],{"class":1574,"line":1270},[1572,21745,21746],{},"    eventsOfInterest: [.keyDown, .keyUp, .flagsChanged],\n",[1572,21748,21749],{"class":1574,"line":4389},[1572,21750,21751],{},"    callback: myEventTapCallback,\n",[1572,21753,21754],{"class":1574,"line":742},[1572,21755,21756],{},"    userInfo: nil\n",[1572,21758,21759],{"class":1574,"line":10336},[1572,21760,11953],{},[11,21762,21763,21764,21767],{},"This requires registering the application for the ",[26,21765,21766],{},"Input Monitoring"," system permission in macOS Settings.",[70,21769,21771],{"id":21770},"auto-generated-key-mappings-via-uckeytranslate","Auto-Generated Key Mappings via UCKeyTranslate",[11,21773,21774,21775,21778],{},"To translate characters cleanly between any pair of languages, the converter needs a key translation map. Instead of hardcoding character tables, EzSwitch queries the Carbon ",[26,21776,21777],{},"UCKeyTranslate"," API on launch.\nIt reads the user's active keyboard layouts, simulates virtual keypresses for all 48 physical key codes under various modifier states (base, Shift, Option, Shift+Option), and compiles a character map on the fly. This ensures the text converter works out of the box with any layout pair installed on the system.",[21646,21780],{"alt":21781,"caption":21782,"layout":21783,"src":21784},"Generated keyboard layout character map","Keyboard layout character map generated at runtime via Carbon UCKeyTranslate API","center","\u002Fimages\u002Fezswitch\u002FezSwitch-mapper.png",[70,21786,21788],{"id":21787},"text-replacement-mechanism","Text Replacement Mechanism",[11,21790,21791,21792,21795],{},"To transform typed text, the app leverages macOS ",[26,21793,21794],{},"Accessibility APIs"," to simulate user keyboard actions:",[4001,21797,21798,21808,21814,21817],{},[301,21799,21800,21801,21685,21804,21807],{},"Simulates ",[49,21802,21803],{},"Shift + Option + Left",[49,21805,21806],{},"Ctrl + Shift + Left"," in IDEs) to select the last word.",[301,21809,21810,21811,8399],{},"Copies the highlighted text to the clipboard (",[49,21812,21813],{},"Cmd+C",[301,21815,21816],{},"Translates the string character-by-character using the active mapping.",[301,21818,21819,21820,8399],{},"Pastes the corrected text back (",[49,21821,21822],{},"Cmd+V",[15,21824,21826],{"id":21825},"conclusion","Conclusion",[11,21828,21829,21830,21834],{},"EzSwitch is a fast, highly optimized open-source utility that integrates cleanly into macOS, saving time during daily multi-lingual workflows. The project is fully open source — check out the ",[38,21831,21833],{"href":21623,"rel":21832},[429],"EzSwitch GitHub repository"," to browse the code, open it in Xcode, or build it yourself.",[11,21836,21837,21838,21840,21841,507],{},"Working on a macOS utility or a tool with complex system integrations? I'm happy to discuss architecture or tricky API challenges — book a ",[38,21839,14222],{"href":12755}," or see ",[38,21842,21844],{"href":21843},"\u002Fabout","what I build and work on",[1779,21846,2290],{},{"title":363,"searchDepth":364,"depth":364,"links":21848},[21849,21853,21858],{"id":21636,"depth":364,"text":21637,"children":21850},[21851,21852],{"id":21643,"depth":370,"text":21644},{"id":21678,"depth":370,"text":21679},{"id":21700,"depth":364,"text":21701,"children":21854},[21855,21856,21857],{"id":21707,"depth":370,"text":21708},{"id":21770,"depth":370,"text":21771},{"id":21787,"depth":370,"text":21788},{"id":21825,"depth":364,"text":21826},"2026-05-17","How I built a lightweight Swift\u002FSwiftUI menu bar utility for smart layout switching and double-tap text transformation on macOS.",{},"\u002Fblog\u002Fen\u002Fezswitch-macos-switcher",{"title":21605,"description":21860},"blog\u002Fen\u002Fezswitch-macos-switcher",[21866,21867,21868,21869,21870,21871,21872],"Mac","Tool","SwiftUI","CLI","Open Source","macOS","Productivity","WB281Vaq8UpYiXpOc2eafkvodpFYVCrhvhp0UzLeCKo",1788079956960]