diff --git a/modules/io/pymod/export_mmcif_io.cc b/modules/io/pymod/export_mmcif_io.cc
index ae6e956fa612f8b9c83575696c48abdea4b0fcc5..cb12690443525ac61625e1f93d50aaf3e76c1646 100644
--- a/modules/io/pymod/export_mmcif_io.cc
+++ b/modules/io/pymod/export_mmcif_io.cc
@@ -26,6 +26,7 @@ using namespace boost::python;
 #include <ost/io/mol/io_profile.hh>
 #include <ost/io/mol/mmcif_reader.hh>
 #include <ost/io/mol/mmcif_info.hh>
+#include <ost/io/mol/mmcif_writer.hh>
 #include <ost/io/mmcif_str.hh>
 using namespace ost;
 using namespace ost::io;
@@ -75,6 +76,11 @@ void export_mmcif_io()
                                    return_value_policy<copy_const_reference>()))
     ;
 
+  class_<MMCifWriter, boost::noncopyable>("MMCifWriter", init<const String&, const IOProfile&>())
+    .def("Process_atom_site", &MMCifWriter::Process_atom_site)
+    .def("Write", &MMCifWriter::Write)
+    ;
+
   enum_<MMCifInfoCitation::MMCifInfoCType>("MMCifInfoCType")
     .value("Journal", MMCifInfoCitation::JOURNAL)
     .value("Book", MMCifInfoCitation::BOOK)
diff --git a/modules/io/src/mol/CMakeLists.txt b/modules/io/src/mol/CMakeLists.txt
index 5f17d12bc4ba6d5c991275b859ce1330253f5551..8d242c3e75422c451d2b9a9b8d13a7b0063a9434 100644
--- a/modules/io/src/mol/CMakeLists.txt
+++ b/modules/io/src/mol/CMakeLists.txt
@@ -17,8 +17,10 @@ chemdict_parser.cc
 io_profile.cc
 dcd_io.cc
 star_parser.cc
+star_writer.cc
 mmcif_reader.cc
 mmcif_info.cc
+mmcif_writer.cc
 pdb_str.cc
 sdf_str.cc
 mmcif_str.cc
@@ -29,9 +31,12 @@ PARENT_SCOPE
 
 set(OST_IO_MOL_HEADERS
 chemdict_parser.hh
+star_base.hh
 star_parser.hh
+star_writer.hh
 mmcif_reader.hh
 mmcif_info.hh
+mmcif_writer.hh
 io_profile.hh
 dcd_io.hh
 entity_io_crd_handler.hh
diff --git a/modules/io/src/mol/mmcif_writer.cc b/modules/io/src/mol/mmcif_writer.cc
new file mode 100644
index 0000000000000000000000000000000000000000..c266d01c20762e32877bed4c829606c04a83c33a
--- /dev/null
+++ b/modules/io/src/mol/mmcif_writer.cc
@@ -0,0 +1,377 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2023 by the OpenStructure authors
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License as published by the Free
+// Software Foundation; either version 3.0 of the License, or (at your option)
+// any later version.
+// This library is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this library; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+
+#include <ost/mol/chem_class.hh>
+
+#include <ost/io/mol/mmcif_writer.hh>
+
+
+
+namespace {
+
+  // generates as many chain names as you want (potentially multiple characters)
+  struct ChainNameGenerator{
+    ChainNameGenerator() { 
+      chain_names = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz";
+      n_chain_names = chain_names.size();
+      indices.push_back(-1);
+    }
+
+    String Get() {
+      int idx = indices.size() - 1;
+      indices[idx] += 1;
+      bool more_digits = false;
+      while(idx >= 0) {
+        if(indices[idx] >= n_chain_names) {
+          indices[idx] = 0;
+          if(idx>0) {
+            indices[idx-1] += 1;
+            --idx;
+          } else {
+            more_digits = true;
+            break;
+          }
+        } else {
+          break;
+        }
+      }
+      if(more_digits) {
+        indices.insert(indices.begin(), 0);
+      }
+      String ch_name(indices.size(), 'X');
+      for(uint i = 0; i < indices.size(); ++i) {
+        ch_name[i] = chain_names[indices[i]];
+      }
+      return ch_name;
+    }
+
+    void Reset() {
+      indices.clear();
+      indices.push_back(-1);
+    }
+
+    String chain_names;
+    int n_chain_names;
+    std::vector<int> indices;
+  };
+}
+
+namespace ost { namespace io {
+
+MMCifWriter::MMCifWriter(const String& filename, const IOProfile& profile):
+  StarWriter(filename),
+  profile_(profile),
+  atom_site_(NULL) { }
+
+MMCifWriter::~MMCifWriter() {
+  if(atom_site_ != NULL) {
+    delete atom_site_;
+  }
+}
+
+void MMCifWriter::Process_atom_site(const ost::mol::EntityHandle& ent) {
+
+  this->Setup_atom_site_();
+
+  std::vector<std::vector<ost::mol::ResidueHandle> > L_chains; // L_PEPTIDE_LINKING
+  std::vector<std::vector<ost::mol::ResidueHandle> > D_chains; // D_PEPTIDE_LINKING
+  std::vector<std::vector<ost::mol::ResidueHandle> > P_chains; // PEPTIDE_LINKING  
+  std::vector<std::vector<ost::mol::ResidueHandle> > R_chains; // RNA_LINKING
+  std::vector<std::vector<ost::mol::ResidueHandle> > S_chains; // DNA_LINKING
+  std::vector<std::vector<ost::mol::ResidueHandle> > X_chains; // L_SACCHARIDE
+  std::vector<std::vector<ost::mol::ResidueHandle> > Y_chains; // D_SACCHARIDE
+  std::vector<std::vector<ost::mol::ResidueHandle> > W_chains; // WATER
+  std::vector<ost::mol::ResidueHandle> N_chains; // NON_POLYMER (1 res per chain)
+  std::vector<ost::mol::ResidueHandle> U_chains; // UNKNOWN (1 res per chain)
+
+  ost::mol::ChainHandleList chain_list = ent.GetChainList();
+  for(auto ch: chain_list) {
+
+    ost::mol::ResidueHandleList res_list = ch.GetResidueList();
+
+    // we don't just go for chain type here...
+    // just think of PDB entries that have a polypeptide, water and a ligand
+    // in the same chain...
+
+    bool has_l_peptide_linking = false;
+    bool has_d_peptide_linking = false;
+    bool has_peptide_linking = false;
+    bool has_rna_linking = false;
+    bool has_dna_linking = false;
+    bool has_l_saccharide = false;
+    bool has_d_saccharide = false;
+    bool has_water = false;
+    for(auto res: res_list) {
+
+      // Peptide chains must not mix L_PEPTIDE_LINKING AND D_PEPTIDE_LINKING
+      if(res.GetChemClass() == ost::mol::ChemClass::D_PEPTIDE_LINKING) {
+        if(has_l_peptide_linking) {
+          throw ost::io::IOException("Cannot write mmCIF when same chain "
+                                     "contains D- and L-peptides");
+        }
+        has_d_peptide_linking = true;
+      }
+      if(res.GetChemClass() == ost::mol::ChemClass::L_PEPTIDE_LINKING) {
+        if(has_d_peptide_linking) {
+          throw ost::io::IOException("Cannot write mmCIF when same chain "
+                                     "contains D- and L-peptides");
+        }
+        has_l_peptide_linking = true;
+      }
+
+      if(res.GetChemClass() == ost::mol::ChemClass::PEPTIDE_LINKING) {
+        has_peptide_linking = true;
+      }
+
+      if(res.GetChemClass() == ost::mol::ChemClass::RNA_LINKING) {
+        has_rna_linking = true;
+      }
+
+      if(res.GetChemClass() == ost::mol::ChemClass::DNA_LINKING) {
+        has_dna_linking = true;
+      }
+
+      if(res.GetChemClass() == ost::mol::ChemClass::L_SACCHARIDE) {
+        has_l_saccharide = true;
+      }
+
+      if(res.GetChemClass() == ost::mol::ChemClass::D_SACCHARIDE) {
+        has_d_saccharide = true;
+      }
+
+      if(res.GetChemClass() == ost::mol::ChemClass::WATER) {
+        has_water = true;
+      }
+    }
+
+    // if there is any L-peptide or D-peptide, all peptides without
+    // chiral center get assigned to it. No need for specific chain.
+    if(has_l_peptide_linking || has_d_peptide_linking) {
+      has_peptide_linking = false;
+    }
+
+    if(has_l_peptide_linking) {
+      L_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_d_peptide_linking) {
+      D_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_peptide_linking) {
+      P_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_rna_linking) {
+      R_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_dna_linking) {
+      S_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_l_saccharide) {
+      X_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_d_saccharide) {
+      Y_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    if(has_water) {
+      W_chains.push_back(ost::mol::ResidueHandleList());
+    }
+
+    for(auto res: res_list) {
+      if(res.GetChemClass().IsPeptideLinking()) {
+        if(has_l_peptide_linking) {
+          L_chains.back().push_back(res);
+        } else if(has_d_peptide_linking) {
+          D_chains.back().push_back(res);
+        } else {
+          P_chains.back().push_back(res);
+        }
+      } else if(res.GetChemClass() == ost::mol::ChemClass::RNA_LINKING) {
+        R_chains.back().push_back(res);
+      } else if(res.GetChemClass() == ost::mol::ChemClass::DNA_LINKING) {
+        S_chains.back().push_back(res);
+      } else if(res.GetChemClass() == ost::mol::ChemClass::L_SACCHARIDE) {
+        X_chains.back().push_back(res);
+      } else if(res.GetChemClass() == ost::mol::ChemClass::D_SACCHARIDE) {
+        Y_chains.back().push_back(res);
+      } else if(res.GetChemClass() == ost::mol::ChemClass::WATER) {
+        W_chains.back().push_back(res);
+      } else if(res.GetChemClass() == ost::mol::ChemClass::NON_POLYMER) {
+        N_chains.push_back(res);
+      } else if(res.GetChemClass() == ost::mol::ChemClass::UNKNOWN) {
+        U_chains.push_back(res);
+      } else {
+        // TODO: make error message more insightful...
+        throw ost::io::IOException("Unsupported chem class...");
+      }
+    }
+  }
+
+  ChainNameGenerator chain_name_gen;
+
+  // process L_PEPTIDE_LINKING
+  for(auto res_list: L_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process D_PEPTIDE_LINKING
+  for(auto res_list: D_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process PEPTIDE_LINKING
+  for(auto res_list: P_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process RNA_LINKING
+  for(auto res_list: R_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process DNA_LINKING
+  for(auto res_list: S_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process L_SACHARIDE
+  for(auto res_list: X_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process D_SACHARIDE
+  for(auto res_list: Y_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process WATER
+  for(auto res_list: W_chains) {
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process NON_POLYMER
+  for(auto res: N_chains) {
+    ost::mol::ResidueHandleList res_list;
+    res_list.push_back(res);
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // process UNKNOWN
+  for(auto res: N_chains) {
+    ost::mol::ResidueHandleList res_list;
+    res_list.push_back(res);
+    String chain_name = chain_name_gen.Get();
+    Feed_atom_site_(chain_name, 0, res_list);
+  }
+
+  // finalize
+  this->Push(atom_site_);
+}
+
+void MMCifWriter::Setup_atom_site_() {
+  StarLoopDesc desc;
+  desc.SetCategory("_atom_site");
+  desc.Add("group_PDB");
+  desc.Add("type_symbol");
+  desc.Add("label_atom_id");
+  desc.Add("label_comp_id");
+  desc.Add("label_asym_id");
+  desc.Add("label_entity_id");
+  desc.Add("label_seq_id");
+  desc.Add("label_alt_id");
+  desc.Add("Cartn_x");
+  desc.Add("Cartn_y");
+  desc.Add("Cartn_z");
+  desc.Add("occupancy");
+  desc.Add("B_iso_or_equiv");
+  desc.Add("auth_seq_id");
+  desc.Add("auth_asym_id");
+  desc.Add("id");
+  atom_site_ = new StarLoop(desc);
+}
+
+void MMCifWriter::Feed_atom_site_(const String& label_asym_id,
+                                  int label_entity_id,
+                                  const ost::mol::ResidueHandleList& res_list) {
+
+  int label_seq_id = 1;
+  for(auto res: res_list) {
+    String comp_id = res.GetName();
+    ost::mol::AtomHandleList at_list = res.GetAtomList();
+    String auth_asym_id = res.GetChain().GetName();
+    String auth_seq_id = res.GetNumber().AsString();
+    for(auto at: at_list) {
+      std::vector<StarLoopDataItemDO> at_data;
+      // group_PDB
+      if(at.IsHetAtom()) {
+        at_data.push_back(StarLoopDataItemDO("HETATM"));
+      } else {
+        at_data.push_back(StarLoopDataItemDO("ATOM"));
+      }
+      // type_symbol
+      at_data.push_back(StarLoopDataItemDO(at.GetElement()));
+      // label_atom_id
+      at_data.push_back(StarLoopDataItemDO(at.GetName()));
+      // label_comp_id
+      at_data.push_back(StarLoopDataItemDO(comp_id));
+      // label_asym_id
+      at_data.push_back(StarLoopDataItemDO(label_asym_id));
+      // label_entity_id
+      at_data.push_back(StarLoopDataItemDO(label_entity_id));
+      // label_seq_id
+      at_data.push_back(StarLoopDataItemDO(label_seq_id));
+      // label_alt_id
+      at_data.push_back(StarLoopDataItemDO("."));
+      // Cartn_x
+      at_data.push_back(StarLoopDataItemDO(at.GetPos().GetX(), 3));
+      // Cartn_y
+      at_data.push_back(StarLoopDataItemDO(at.GetPos().GetY(), 3));
+      // Cartn_z
+      at_data.push_back(StarLoopDataItemDO(at.GetPos().GetZ(), 3));
+      // occupancy
+      at_data.push_back(StarLoopDataItemDO(at.GetOccupancy(), 2));
+      // B_iso_or_equiv
+      at_data.push_back(StarLoopDataItemDO(at.GetBFactor(), 2));
+      // auth_seq_id
+      at_data.push_back(StarLoopDataItemDO(auth_seq_id));
+      // auth_asym_id
+      at_data.push_back(StarLoopDataItemDO(auth_asym_id));
+      // id
+      at_data.push_back(StarLoopDataItemDO(atom_site_->GetN()));
+      atom_site_->AddData(at_data);
+    }
+    ++label_seq_id;
+  }
+}
+
+}} // ns
diff --git a/modules/io/src/mol/mmcif_writer.hh b/modules/io/src/mol/mmcif_writer.hh
new file mode 100644
index 0000000000000000000000000000000000000000..f5b5ece376fec5b801c0ccae66ca17e1853f1c09
--- /dev/null
+++ b/modules/io/src/mol/mmcif_writer.hh
@@ -0,0 +1,55 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2023 by the OpenStructure authors
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License as published by the Free
+// Software Foundation; either version 3.0 of the License, or (at your option)
+// any later version.
+// This library is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this library; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+#ifndef OST_IO_MMCIF_WRITER_HH
+#define OST_IO_MMCIF_WRITER_HH
+
+#include <fstream>
+
+#include <ost/mol/entity_handle.hh>
+
+#include <ost/io/mol/mmcif_info.hh>
+#include <ost/io/mol/io_profile.hh>
+#include <ost/io/mol/star_writer.hh>
+
+namespace ost { namespace io {
+
+class DLLEXPORT_OST_IO MMCifWriter : public StarWriter {
+public:
+
+  MMCifWriter(const String& filename, const IOProfile& profile);
+
+  virtual ~MMCifWriter();
+
+  void Process_atom_site(const ost::mol::EntityHandle& ent);
+
+private:
+
+  void Setup_atom_site_();
+  void Feed_atom_site_(const String& label_asym_id,
+                       int label_entity_id,
+                       const ost::mol::ResidueHandleList& res_list);
+
+  IOProfile profile_;
+  StarLoop* atom_site_;
+};
+
+ 
+}} // ns
+
+#endif
\ No newline at end of file
diff --git a/modules/io/src/mol/star_base.hh b/modules/io/src/mol/star_base.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2433a0af6bb9deed53657bc636f7a60982c73b11
--- /dev/null
+++ b/modules/io/src/mol/star_base.hh
@@ -0,0 +1,307 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2023 by the OpenStructure authors
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License as published by the Free
+// Software Foundation; either version 3.0 of the License, or (at your option)
+// any later version.
+// This library is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this library; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+#ifndef OST_IO_STAR_BASE_HH
+#define OST_IO_STAR_BASE_HH
+
+#include <map>
+#include <algorithm>
+#include <ost/string_ref.hh>
+#include <ost/io/io_exception.hh>
+
+namespace{
+  // float to string with specified number of decimals
+  void fts(Real f, int decimals, String& s) {
+    char data[20];
+    size_t len;
+    switch(decimals){
+      case 0:
+        len = std::snprintf(data, sizeof(data), "%.0f", f);
+        break;
+      case 1:
+        len = std::snprintf(data, sizeof(data), "%.1f", f);
+        break;
+      case 2:
+        len = std::snprintf(data, sizeof(data), "%.2f", f);
+        break;
+      case 3:
+        len = std::snprintf(data, sizeof(data), "%.3f", f);
+        break;
+      case 4:
+        len = std::snprintf(data, sizeof(data), "%.4f", f);
+        break;
+      case 5:
+        len = std::snprintf(data, sizeof(data), "%.5f", f);
+        break;
+      case 6:
+        len = std::snprintf(data, sizeof(data), "%.6f", f);
+        break;
+      default:
+        throw ost::io::IOException("Max decimals in float conversion: 6");
+    }
+  
+    if(len < 0 || len > 20) {
+      throw ost::io::IOException("float conversion failed");
+    }
+    s.assign(data, len);
+  }
+}
+
+namespace ost { namespace io {
+
+
+typedef enum {
+  STAR_DIAG_WARNING,
+  STAR_DIAG_ERROR
+} StarDiagType;
+
+
+class DLLEXPORT_OST_IO StarObject {
+public:
+  virtual ~StarObject() { }
+  virtual void ToStream(std::ostream& s) = 0;
+};
+
+class DLLEXPORT_OST_IO StarDataItem : public StarObject{
+public:
+  StarDataItem(const StringRef& category, const StringRef& name, 
+                const StringRef& value): 
+    category_(category), name_(name), value_(value)
+  { }
+
+  virtual void ToStream(std::ostream& s) {
+    s << category_ << '.' << name_ << ' ' << value_ << std::endl;
+  }
+
+  const StringRef& GetCategory() const { return category_; }
+  const StringRef& GetName() const { return name_; }
+  const StringRef& GetValue() const { return value_; }
+private:
+  StringRef category_;
+  StringRef name_;
+  StringRef value_;
+};
+
+
+class DLLEXPORT_OST_IO StarDataItemDO : public StarObject{
+  // basically a copy of StarDataItem but with data ownership
+  // Problem with StringRef: It's purely pointer based
+  // => data needs to reside elsewhere
+public:
+  StarDataItemDO(const String& category, const String& name, 
+                 const String& value): 
+    category_(category), name_(name)
+  {
+    // cases we still need to deal with:
+    // - special characters in strings (put in quotation marks)
+    // - long strings (semicolon based syntax)
+    // see https://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html
+    if(value == "") {
+      value_ = ".";
+    } else {
+      value_ = value;
+    }
+  }
+
+  StarDataItemDO(const String& category, const String& name, 
+                 Real value, int decimals): 
+    category_(category), name_(name)
+  {
+    fts(value, decimals, value_);
+  }
+
+  StarDataItemDO(const String& category, const String& name, 
+                 int value): 
+    category_(category), name_(name)
+  {
+    value_ = std::to_string(value);
+  }
+
+  virtual void ToStream(std::ostream& s) {
+    s << category_ << '.' << name_ << ' ' << value_ << std::endl;
+  }
+
+  const String& GetCategory() const { return category_; }
+  const String& GetName() const { return name_; }
+  const String& GetValue() const { return value_; }
+private:
+  String category_;
+  String name_;
+  String value_;
+};
+
+class DLLEXPORT_OST_IO StarDataCategoryDO : public StarObject {
+public:
+  StarDataCategoryDO(const String& category): category_(category) {}
+
+  void Add(const StarDataItemDO& data_item) {
+    if(data_item.GetCategory() != category_) {
+      throw ost::io::IOException("category mismatch");
+    }
+    data_items_.push_back(data_item);
+  }
+
+  virtual void ToStream(std::ostream& s) {
+    for(auto it = data_items_.begin(); it != data_items_.end(); ++it) {
+      it->ToStream(s);
+    }
+  }
+
+private:
+  String category_;
+  std::vector<StarDataItemDO> data_items_;
+};
+
+class DLLEXPORT_OST_IO StarLoopDesc : public StarObject {
+public:
+  StarLoopDesc():
+    category_("")
+  { }
+  
+  int GetIndex(const String& name) const
+  {
+    std::map<String, int>::const_iterator i=index_map_.find(name);
+    return i==index_map_.end() ? -1 : i->second;
+  }
+  
+  void SetCategory(const StringRef& category)
+  {
+    category_=category.str();
+  }
+
+  void SetCategory(const String& category)
+  {
+    category_=category;
+  }  
+
+  void Add(const StringRef& name)
+  {
+    index_map_.insert(std::make_pair(name.str(), index_map_.size()));
+  }
+  void Add(const String& name)
+  {
+    index_map_.insert(std::make_pair(name, index_map_.size()));
+  }
+  size_t GetSize() const 
+  {
+    return index_map_.size();
+  }
+  void Clear()
+  {
+    category_.clear();
+    index_map_.clear();
+  }
+
+  virtual void ToStream(std::ostream& s) {
+    std::vector<std::pair<int, String> > tmp;
+    for(auto it = index_map_.begin(); it != index_map_.end(); ++it) {
+      tmp.push_back(std::make_pair(it->second, it->first));
+    }
+    std::sort(tmp.begin(), tmp.end());
+    for(auto it = tmp.begin(); it != tmp.end(); ++it) {
+      s << category_ << "." << it->second << std::endl;
+    }
+  }
+
+  const String& GetCategory() const { return category_; }
+private:
+  String                category_;
+  std::map<String, int> index_map_;
+};
+
+class DLLEXPORT_OST_IO StarLoopDataItemDO{
+public:
+
+  StarLoopDataItemDO(const String& value) {
+    // cases we still need to deal with:
+    // - special characters in strings (put in quotation marks)
+    // - long strings (semicolon based syntax)
+    // see https://mmcif.wwpdb.org/docs/tutorials/mechanics/pdbx-mmcif-syntax.html
+    if(value == "") {
+      value_ = ".";
+    } else {
+      value_ = value;
+    }
+  }
+
+  StarLoopDataItemDO(Real value, int decimals) {
+    fts(value, decimals, value_);
+  }
+
+  StarLoopDataItemDO(int value) {
+    value_ = std::to_string(value);
+  }
+
+  const String& GetValue() const { return value_; }
+
+  virtual void ToStream(std::ostream& s) {
+    s << value_;
+  }
+
+private:
+  String value_;
+};
+
+class DLLEXPORT_OST_IO StarLoop: public StarObject {
+public:
+
+  StarLoop() { }
+
+  StarLoop(const StarLoopDesc& desc): desc_(desc) { }
+
+  void SetDesc(const StarLoopDesc& desc) {
+    if(!data_.empty()) {
+      throw ost::io::IOException("Can only set new StarLoop desc in "
+                                 "in empty loop");
+    }
+    desc_ = desc;
+  }
+
+  void AddData(const std::vector<StarLoopDataItemDO>& data) {
+    if(data.size() != desc_.GetSize()) {
+      throw ost::io::IOException("Invalid data size when adding to StarLoop");
+    }
+    data_.insert(data_.end(), data.begin(), data.end());
+  }
+
+  int GetN() {
+    return data_.size() / desc_.GetSize();
+  }
+
+  virtual void ToStream(std::ostream& s) {
+    s << "loop_" << std::endl;
+    desc_.ToStream(s);
+    int desc_size = desc_.GetSize();
+    for(size_t i = 0; i < data_.size(); ++i) {
+      data_[i].ToStream(s);
+      if((i+1) % desc_size == 0) {
+        s << std::endl;
+      } else {
+        s << ' ';
+      }
+    }
+  }
+
+private:
+  StarLoopDesc desc_;
+  std::vector<StarLoopDataItemDO> data_;
+};
+ 
+}} // ns
+
+#endif
diff --git a/modules/io/src/mol/star_parser.hh b/modules/io/src/mol/star_parser.hh
index bc6a3947a6694ec83e8e3fd699d30223cc87359f..0d13b1a50ab4be12b2da85c72adf725f4bee4d59 100644
--- a/modules/io/src/mol/star_parser.hh
+++ b/modules/io/src/mol/star_parser.hh
@@ -31,70 +31,10 @@
 #include <map>
 #include <ost/string_ref.hh>
 #include <ost/io/module_config.hh>
+#include <ost/io/mol/star_base.hh>
 
 namespace ost { namespace io {
 
-
-typedef enum {
-  STAR_DIAG_WARNING,
-  STAR_DIAG_ERROR
-} StarDiagType;
-
-
-class DLLEXPORT_OST_IO StarDataItem {
-public:
-  StarDataItem(const StringRef& category, const StringRef& name, 
-                const StringRef& value): 
-    category_(category), name_(name), value_(value)
-  { }
-  const StringRef& GetCategory() const { return category_; }
-  const StringRef& GetName() const { return name_; }
-  const StringRef& GetValue() const { return value_; }
-private:
-  StringRef category_;
-  StringRef name_;
-  StringRef value_;
-};
-
-class DLLEXPORT_OST_IO StarLoopDesc {
-public:
-  StarLoopDesc():
-    category_("")
-  { }
-  
-  int GetIndex(const String& name) const
-  {
-    std::map<String, int>::const_iterator i=index_map_.find(name);
-    return i==index_map_.end() ? -1 : i->second;
-  }
-  
-  void SetCategory(const StringRef& category)
-  {
-    category_=category.str();
-  }
-  
-
-
-  void Add(const StringRef& name)
-  {
-    index_map_.insert(std::make_pair(name.str(), index_map_.size()));
-  }
-  size_t GetSize() const 
-  {
-    return index_map_.size();
-  }
-  void Clear()
-  {
-    category_.clear();
-    index_map_.clear();
-  }
-
-  const String& GetCategory() const { return category_; }
-private:
-  String                category_;
-  std::map<String, int> index_map_;
-};
-
 /// \brief parser for the STAR file format
 /// 
 /// \section star_format STAR format description
diff --git a/modules/io/src/mol/star_writer.cc b/modules/io/src/mol/star_writer.cc
new file mode 100644
index 0000000000000000000000000000000000000000..327d005631c6e656e1e89e954cb9446c8e78c33d
--- /dev/null
+++ b/modules/io/src/mol/star_writer.cc
@@ -0,0 +1,56 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2023 by the OpenStructure authors
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License as published by the Free
+// Software Foundation; either version 3.0 of the License, or (at your option)
+// any later version.
+// This library is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this library; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+
+#include <ost/io/mol/star_writer.hh>
+
+namespace ost{ namespace io{
+
+StarWriter::StarWriter(std::ostream& stream): filename_("<stream>"),
+                                              file_open_(true) {
+  if(!stream) {
+    file_open_ = false;
+  }
+  stream_.push(stream);
+}
+
+
+StarWriter::StarWriter(const String& filename): filename_(filename),
+                                                file_open_(true),
+                                                fstream_(filename.c_str()) {
+  if (!fstream_) {
+    file_open_ = false;
+  }
+  stream_.push(fstream_);
+}
+
+void StarWriter::Write(const String& data_name) {
+  if (!file_open_) {
+    throw IOException("yolo");
+  }
+
+  // write data header
+  stream_ << "data_" << data_name << std::endl;
+
+  for(auto star_obj : categories_to_write_) {
+    star_obj->ToStream(stream_);
+    stream_ << String("#") << std::endl;
+  }
+}
+
+}} // ns
diff --git a/modules/io/src/mol/star_writer.hh b/modules/io/src/mol/star_writer.hh
new file mode 100644
index 0000000000000000000000000000000000000000..f858e021a81ff6e0e012dd29c7c6f4291258c1ac
--- /dev/null
+++ b/modules/io/src/mol/star_writer.hh
@@ -0,0 +1,50 @@
+//------------------------------------------------------------------------------
+// This file is part of the OpenStructure project <www.openstructure.org>
+//
+// Copyright (C) 2008-2023 by the OpenStructure authors
+//
+// This library is free software; you can redistribute it and/or modify it under
+// the terms of the GNU Lesser General Public License as published by the Free
+// Software Foundation; either version 3.0 of the License, or (at your option)
+// any later version.
+// This library is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with this library; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+//------------------------------------------------------------------------------
+#ifndef OST_IO_STAR_WRITER_HH
+#define OST_IO_STAR_WRITER_HH
+
+#include <map>
+#include <fstream>
+#include <boost/iostreams/filtering_stream.hpp>
+#include <boost/iostreams/filter/gzip.hpp>
+#include <ost/string_ref.hh>
+#include <ost/io/mol/star_base.hh>
+
+
+namespace ost { namespace io {
+
+class DLLEXPORT_OST_IO StarWriter {
+public:
+  StarWriter(std::ostream& stream);
+  StarWriter(const String& filename);
+  virtual ~StarWriter() { }
+
+  void Push(StarObject* obj) { categories_to_write_.push_back(obj); }
+  void Write(const String& data_name);
+private:
+  String filename_;
+  bool file_open_;
+  std::ofstream fstream_;
+  boost::iostreams::filtering_stream<boost::iostreams::output> stream_;
+  std::vector<StarObject*> categories_to_write_;
+};
+
+}} // ns
+
+#endif