Host in your own AWS cloud on top of serverless infrastructure. Increase the reliability, performance and security of your content platform under enterprise workloads, and reduce your infrastructure spend.
RECOGNIZED BY GARTNER DIGITAL MARKETS
We take a different approach to the rest of the market. We cut the cost of the DIY approach, whilst allowing the customizability beyond what any SaaS solution can provide, all whilst keeping you in control of your data.
World’s leading companies use Webiny to create, manage and deliver content at scale while keeping privacy at the forefront.
Siemens chose Webiny to power their global website.
Main reasons why Siemens went with Webiny.
While evaluating all the options on the market, and also the potential of a DIY approach, Webiny stood out as the best platform when it came to meeting Siemens' demands in terms of customization, scale, performance and feature set.
Siemens decided Webiny was the best platform to power siemens.com, 300 regional offices, across 60 languages and with over 1200 content writers creating content.
Size of the data set that was migrated to Webiny.
The most praised thing about working with Webiny.
Motortrend, a Warner Bros. Discovery (WBD) brand, chose Webiny as their content platform as it was the only one on the market that met the high bar of scale and customization.
Webiny as a foundation for a multi-region, multi-language project.
Serverless and open-source made Webiny an easy choice.
AWS was in the process or rebuilding one of their global product offerings for which they needed a highly-scalable and very customizable platform to use as a foundation in order to deliver the project on budget and in time. Webiny stood out from the crowd due to it's unique offering and serverless infrastructure.
Delivering scalable and uninterrupted service during peak usage.
Ability to tailor the loyalty program experience to an individual customer.
Starbucks, a global coffeehouse leader, recently revolutionized its loyalty program mobile application by integrating Webiny as the backend solution. This strategic move aimed to enhance customer experiences through serverless infrastructure and unparalleled customization options.
A fully customizable end-to-end solution designed with performance, reliability and security in mind,
for your most complex and critical projects. With Webiny you are not limited to the experiences the system allows
you to create, but only by your imagination.
It’s a no-code suite of products helping you create, manage
and distribute both content and assets
Use Webiny Headless CMS when you need:
Use Webiny Page Builder when you need to:
Use Webiny File Manager when you need to:
Use Webiny Advanced Publishing Workflow when you need to:
Use Webiny Form Builder when you need to:
The end-to-end platform that Webiny provides solves challenges around data ownership, customizations,
infrastructure cost, scalability & reliability and helps you manage the full content lifecycle.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras iaculis et libero eget
eleifend. Vestibulum felis ipsum, aliquet eget semper at, eleifend non nisl.
Host thousands of projects from a single instance
Architected to be extended and customized.
Your data under your terms. A privacy-focused CMS.
Webiny runs on highly-scalable fault-tolerant serverless services.
Build new features, change existing ones, or create whole new apps.
No-code suite of solutions helping you create, manage and distribute content.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras iaculis et libero eget eleifend. Vestibulum felis ipsum, aliquet eget semper at, eleifend non nisl.
Type definitions across the whole project to help you get around.
Using Webiny CLI you can propagate code trough different environments, like dev, prod.
Change existing GraphQL resolvers, or add new ones in a few lines of code.
Webiny is deployed inside your AWS account following all security best practices.
You can integrate any of your existing IdPs.
Control and modify your infrastructure through Pulumi IaC.
70+
Contributors
2,700+
Developers on Slack
7,200+
GitHub stars
Webiny is architected from the ground up to be adopted, extended and built upon. You can literally change every part of Webiny, in a safe and maintainable way.
Lifecycle events are added on top of folder actions which can be used to trigger custom functions.
12new ContextPlugin<AcoContext>(async context => {3 context.aco.onFolderAfterCreate.subscribe(async ({ folder }) => {4 // Trigger custom function here5 console.log('Folder created', folder.title);6 });7});8
When selecting multiple entries, you can register custom bulk actions that the user can perform.
12<ContentEntryListConfig>3 <BulkAction4 name={"copy-json"}5 element={<ActionCopyJson />}6 modelIds={["article"]}7 />8</ContentEntryListConfig>9
You can register custom filters to filter the entries in the list.
12<ContentEntryListConfig>3 <Browser.Filter name={"demo-filter"} element={<span>Demo Filter</span>} />4</ContentEntryListConfig>5
Configure on a per-model basis which columns are shown in the list view.
12<ContentEntryListConfig>3 <Browser.Column4 name={"price"}5 header={"Price"}6 modelIds={["property"]}7 />8</ContentEntryListConfig>9
Control how a column is rendered in the list view.
12export const CellPrice = () => {3 // You can destructure child methods to make the code more readable and easier to work with.4 const { useTableRow, isFolderRow } = ContentEntryListConfig.Browser.Table.Column;5 // useTableRow() allows you to access the entire data of the current row.6 const { row } = useTableRow();78 // isFolderRow() allows for custom rendering when the current row is a folder.9 if (isFolderRow(row)) {10 return <>{"-"}</>;11 }1213 const currency = new Intl.NumberFormat("en-US", {14 style: "currency",15 currency: row.currency // Let's use the currency defined in the entry.16 });1718 // Let's render the entry price.19 return <>{currency.format(row.price)}</>;20};21<ContentEntryListConfig>22 <Browser.Column23 name={"price"}24 header={"Price"}25 modelIds={["property"]}26 cell={<CellPrice />}27 />28</ContentEntryListConfig>29
Customize how search query works and how the input parameters are handled.
12export const CellPrice = () => {3 // You can destructure child methods to make the code more readable and easier to work with.4 const { useTableRow, isFolderRow } = ContentEntryListConfig.Browser.Table.Column;5 // useTableRow() allows you to access the entire data of the current row.6 const { row } = useTableRow();78 // isFolderRow() allows for custom rendering when the current row is a folder.9 if (isFolderRow(row)) {10 return <>{"-"}</>;11 }1213 const currency = new Intl.NumberFormat("en-US", {14 style: "currency",15 currency: row.currency // Let's use the currency defined in the entry.16 });1718 // Let's render the entry price.19 return <>{currency.format(row.price)}</>;20};21<ContentEntryListConfig>22 <Browser.Column23 name={"price"}24 header={"Price"}25 modelIds={["property"]}26 cell={<CellPrice />}27 />28</ContentEntryListConfig>29
Remove or register new actions that can be performed on an entry.
12<ContentEntryListConfig>3 <Browser.EntryAction4 name={"copy-json"}5 element={<CopyEntryData />}6 modelIds={["property"]}7 />8</ContentEntryListConfig>9
Register a custom element group
12export default {3 name: "pb-editor-element-group-webiny-website",4 type: "pb-editor-page-element-group",5 group: {6 title: "Webiny Website",7 icon: <Icon />8 }9} as PbEditorPageElementGroupPlugin;10
Register a custom element you can use to build your page
12const plugin = {3 name: "pb-render-page-element-space-x",4 type: "pb-render-page-element",5 elementType: "spaceX",6 render: SpaceX7} as PbRenderElementPlugin;8
Create advanced nestable elements and control where and how they can be nested
12const plugin = {3 name: "pb-render-page-element-child-example",4 type: "pb-render-page-element",5 elementType: "childExample",6 render: ChildExample,7 // Whitelist elements that can accept this element8 // (for drag&drop interaction)9 target: ["cell", "block"],10} as PbEditorPageElementPlugin;11
Create elements that are interactive and can also fetch data from external sources
12export const SpaceX = createRenderer(() => {3 // Let's retrieve the variables that were chosen by4 // the user upon dropping the page element onto the page.5 const { getElement } = useRenderer();6 const element = getElement<SpaceXElementData>();7 const { limit, offset, type } = element.data.variables;89 const [data, setData] = useState<Spacecraft[]>([]);1011 // This is where we fetch the data and store it into component's state.12 useEffect(() => {13 request(GQL_API_URL, QUERIES[type], {14 limit: parseInt(limit),15 offset: parseInt(offset)16 }).then(({ data }) => setData(data));17 }, [limit, offset, type]);1819 if (!data.length) {20 return <>Nothing to show.</>;21 }2223 return <>SpaceX has {data.length} rockets</>;24});2526const plugin = {27 name: "pb-render-page-element-space-x",28 type: "pb-render-page-element",29 elementType: "spaceX",30 render: SpaceX31} as PbRenderElementPlugin;32
Register custom plugins to define new style props, or remove existing style props
12export default {3 name: "pb-editor-page-element-style-settings-text",4 type: "pb-editor-page-element-style-settings",5 render({ options }) {6 return <TextSettings options={options} />;7 }8} as PbEditorPageElementStyleSettingsPlugin;9
Register custom attributes for your custom elements. Example, define which category of products will be listed inside your custom listing component.
12export default {3 name: "pb-editor-page-element-advanced-settings-carousel",4 type: "pb-editor-page-element-advanced-settings",5 elementType: "carousel",6 render() {7 return <CarouselItems />;8 }9} as PbEditorPageElementAdvancedSettingsPlugin;10
Remove page settings props you don't need. Create custom page settings props for your own needs.
12export default [3 // Add 'password' to the page settings types4 new GraphQLSchemaPlugin<Context>({5 typeDefs: /* GraphQL */ `6 extend type PbGeneralPageSettings {7 password: String8 }910 extend input PbGeneralPageSettingsInput {11 password: String12 }13 `14 }),15 // Subscribe to the page update event using the ContextPlugin.16 new ContextPlugin<PbContext>(({ pageBuilder }) => {17 // We are passing a custom event type to allow us to use the new 'password' field.18 pageBuilder.onBeforePageUpdate.subscribe<CustomEventParams>(({ page, input }) => {19 // Explicitly assign the field value from GraphQL input to the data that is used to update the page.20 page.settings.general.password = input.settings.general.password;21 });22 })23 ];24
Take over the publish button action and trigger a custom action. Or an action that happens before or and after the page publish event.
12new ContextPlugin<PbContext>(async context => {3 context.pageBuilder.onBeforePagePublish.subscribe(async ({ latestPage, page }) => {4 /**5 * For example, we do not allow a page which is not the latest one to be published.6 */7 if (latestPage.version > page.version) {8 throw new Error(`Page you are trying to publish is not the latest revision of the page.`);9 }10 });11 });12
Register a plugin to add a new file type handler to the file manager
12export default [3 new FileManagerFileTypePlugin({4 types: ["video/mp4"],5 render({ file }) {6 return (7 <div style={{ paddingTop: "40%" }}>8 <strong>My MP4</strong>9 <br />10 <span>{file.name}</span>11 <br />12 <span>{file.size} bytes</span>13 </div>14 );15 }16 })17];18
Register your own custom File Manager UI, useful if you want to use a different DAM system, something like Cloudinary, Dropbox, etc.
12const CustomFileManager = createDecorator(FileManagerRenderer, () => {3 return function FileManagerRenderer(props) {4 const setRandomImage = () => {5 const id = Date.now().toString();6 const image: FileManagerFileItem = {7 id,8 src: `https://picsum.photos/seed/${id}/200/300`,9 meta: [{ key: "source", value: "https://picsum.photos/" }]10 };11 if (props.multiple) {12 props.onChange && props.onChange([image]);13 } else {14 props.onChange && props.onChange(image);15 }16 props.onClose && props.onClose();17 };1819 return (20 <OverlayLayout onExited={() => props.onClose && props.onClose()}>21 {/* Render a simple button, and assign a random image on click. */}22 <button onClick={setRandomImage}>Set random image</button>23 </OverlayLayout>24 );25 };26 });2728 export const App = () => {29 return (30 <Admin>31 <Cognito />32 {/* Mount the plugin, which will register a HOC for the `FileManagerRenderer`. */}33 <CustomFileManager />34 </Admin>35 );36 };37
Create a custom filter that can be used to filter files in the File Manager
12const { Browser } = FileManagerViewConfig;34const DemoFilter = () => {5 return <span>Demo Filter</span>;6}78export const App = () => {9 return (10 <Admin>11 <Cognito />12 <FileManagerViewConfig>13 <Browser.Filter name={"new-filter"} element={<DemoFilter />} />14 </FileManagerViewConfig>15 </Admin>16 );17};18
Add custom meta data fields to your files
12export const handler = createHandler({3 plugins: [4 // Other plugins were omitted for clarity.56 // Add the following code after your existing plugins.7 createFileModelModifier(({ modifier }) => {8 modifier.addField({9 id: "carMake",10 fieldId: "carMake",11 label: "Car Make",12 type: "text",13 renderer: {14 name: "text-input"15 }16 });1718 modifier.addField({19 id: "year",20 fieldId: "year",21 label: "Year of manufacturing",22 type: "number",23 renderer: {24 name: "number-input"25 }26 });27 })28 ],29 http: { debug }30});31
Change the UI of the details drawer. You can change the size of the drawer, hide fields, and group fields.
12<FileManagerViewConfig>3 {/* Use percentage value. */}4 <FileDetails.Width value={"80%"} />5 {/* Use pixel value. */}6 <FileDetails.Width value={"1300px"} />7</FileManagerViewConfig>8
You can listen to file lifecycle events, such as file upload, file delete, etc. and trigger custom functions.
12new ContextPlugin<FileManagerContext>(async context => {3 context.fileManager.onFileAfterCreate.subscribe(async ({ file }) => {4 // Send a notification to Slack, or any other service.5 await sendNotificationToSlack({6 text: `New file created: ${file.name}`7 });8 });9 });10
Extend the GraphQL types and operations
12new CmsGraphQLSchemaPlugin<Context>({3 // Extend the `Query` type with the `listMyPosts` query. Note the `PostListResponse` type.4 // It exists because we've previously created the `Post` content model via Admin Area.5 typeDefs: /* GraphQL */ `6 extend type Query {7 # List posts that were created by the currently logged in user.8 listMyPosts: PostListResponse9 }10 `,11 // In order for the `listMyPosts` to work, we also need to create a resolver function.12 resolvers: {13 Query: {14 listMyPosts: async (_, args: { id: string }, context) => {15 const { security, cms } = context;1617 // Retrieve the `post` model.18 const model = await cms.models.get("post");1920 // Use the `cms.entries.listLatest` method to fetch latest entries for the currently21 // logged in user. Note that you could also use the `listPublished` method here instead22 // of `cms.entries.listLatest`, if a list of published pages is what you need.23 const response: [CmsContentEntry[], CmsContentEntryMeta] = await cms.entries.listLatest(24 model,25 {26 where: {27 // Retrieving the currently logged is as easy as calling the security.getIdentity method.28 createdBy: security.getIdentity().id29 }30 }31 );3233 return new ListResponse(...response);34 }35 }36 }37 })38
Create custom sorting for user defined content models
12export const customSorterPlugin = createCmsGraphQLSchemaSorterPlugin(({ sorters, model }) => {3 // we only want to add the sorter when generating a certain model GraphQL Schema4 if (model.modelId !== "yourTargetModelId") {5 return sorters;6 }7 return [...sorters, "myCustomSorting_ASC", "myCustomSorting_DESC"];8});9
Code-based content models can be used to implement custom business logic and make it easier to version your schema changes
12export default [3 // Defines a new "E-Commerce" content models group.4 new CmsGroupPlugin({5 id: "ecommerce",6 name: "E-Commerce",7 description: "E-Commerce content model group",8 slug: "e-commerce",9 icon: "fas/shopping-cart"10 }),1112 // Defines a new "Product" content model.13 new CmsModelPlugin({14 name: "Product",15 modelId: "product",16 description: "Product content model",17 group: {18 id: "ecommerce",19 name: "E-Commerce"20 },21 fields: [22 {23 id: "productName",24 fieldId: "productName",25 type: "text",26 label: "Product Name",27 helpText: "A short product name",28 renderer: { name: "text-input" },29 validation: [30 {31 name: "required",32 message: "Value is required."33 }34 ]35 },36 {37 id: "productSku",38 fieldId: "productSku",39 type: "text",40 label: "SKU",41 placeholderText: "SKU = Stock Keeping Unit",42 renderer: { name: "text-input" }43 },44 {45 id: "productPrice",46 fieldId: "productPrice",47 type: "number",48 label: "Price",49 renderer: { name: "text-input" }50 }51 ],52 layout: [["productName"], ["productSku", "productPrice"]],53 titleFieldId: "productName"54 })55 ];56
Create custom functions that are executed at specific points in the lifecycle of a content entry
12new ContextPlugin<CmsContext>(async context => {3 context.cms.onEntryAfterUpdate.subscribe(async ({ model, entry }) => {4 /**5 * For example, notify another system about updated entry.6 */7 await notifyAnotherSystemAboutEntryUpdate({ model, entry });8 });9});10
Transform your data before it is stored in the database, and before it is returned to the client
12new StorageTransformPlugin({3 fieldType: "time",4 fromStorage: async ({ value }) => {5 const hours = Math.floor(value / 3600);6 const secondsAfterHours = value - hours * 3600;7 const minutes = secondsAfterHours > 0 ? Math.floor(secondsAfterHours / 60) : 0;8 const seconds = secondsAfterHours - minutes * 60;910 return [hours, minutes, seconds].map(value => String(value).padStart(2, "0")).join(":");11 },12 toStorage: async ({ value }) => {13 const [hours, minutes, seconds] = value.split(":").map(Number);1415 return hours * 3600 + minutes * 60 + seconds;16 }17 });18
Historically self-hosting has been hard, but the times have changed. Serverless infrastructure significantly reduces the operational costs of self-hosting, while giving you an infrastructure setup that's more secure and more scalable than what many off-the-shelf SaaS solutions can provide.
Want more proof? Give us a call, we're happy to show you.
The underlying serverless infrastructure is fault-tollerant, and can scale to handle workloads from your very first user, to your first million users, and beyond.
Webiny is self-hosted, your data is your own, nobody else has access to it.
It’s easy to check-in, but also to check-out. No lock-in!
The consumption-based pricing model of serverless infrastructure ensures you never overpay or overprovision your infrastructure ever again.
The infrastructure is maintained by the cloud vendor for you. It will save you time and money, and reduce potential attack vectors, while making you more secure.
Webiny being self-hosted means you can apply your own data governance requirements.
Webiny is SOC2 compliant out-of-the-box, but you don't need to stop there, GDPR, FedRAMP and HIPPA compliances can also be met.
Less time spent on infrastructure management unlocks more time spent doing the work that really matterrs, like building better products.
Webiny provides integration points to bring any existing IdP or SSO setup, including any custom made ones.
Webiny can be deployed inside an existing VPC. What’s more, this allows Webiny to communicate and tightly integrate with all your current systems and applications in a secure way.
We believe any security that's based on "trust" is a bad practice. With most SaaS vendors instead of zero-trust approach, you get the "trust me" approach.
Webiny is the opposite, you own your data and control your security.
Bring and integrate your own Firewall, IDPS and monitoring system.
Discuss your business/project needs and CMS requirements
Get the answers to your specific business questions
See Webiny in action and learn how it can power your business
Install Webiny in just 4 minutes.
Learn how to create a new Webiny project and deploy it into your AWS account.